RxJava is a Java VM implementation of Reactive Extensions (ReactiveX): a library for composing asynchronous and event-based programs using observable sequences. It extends the observer pattern to support streams of data and events, and adds a large catalogue of operators that let you compose those streams declaratively — while the library handles low-level threading, synchronization, thread-safety, and concurrent data structures for you.
This guide covers the 4.x line of RxJava, whose classes live under the io.reactivex.rxjava4 package and whose Maven coordinates are io.reactivex.rxjava4:rxjava. RxJava 4 is a native Java 26 implementation built on java.util.concurrent.Flow, with first-class support for virtual threads.
Note
RxJava 4 is under active development (current build version
4.0.0-SNAPSHOT). The classic reactive types (Flowable,Observable,Single,Maybe,Completable) and their operators are stable and battle-tested across 2.x/3.x. The newer virtual-thread APIs (Streamable,Schedulers.virtual(),virtualCreate) are new in 4.x and still evolving — see Virtual threads andStreamable.
1. Quick Start
1.1 Add the dependency
RxJava 4 ships as a single JAR with no third-party runtime dependencies.
Gradle (Kotlin/Groovy DSL)
implementation "io.reactivex.rxjava4:rxjava:4.x.y"
Maven
<dependency>
<groupId>io.reactivex.rxjava4</groupId>
<artifactId>rxjava</artifactId>
<version>x.y.z</version>
</dependency>
Ivy
<dependency org="io.reactivex.rxjava4" name="rxjava" rev="x.y.z" />
Replace x, y, z with the latest published version. Released binaries are on Maven Central; pre-release snapshots are published separately (see Installation).
1.2 Hello World
package rxjava.examples;
import io.reactivex.rxjava4.core.*;
public class HelloWorld {
public static void main(String[] args) {
Flowable.just("Hello world").subscribe(System.out::println);
}
}
Hello world
Important
In RxJava 4 the base classes and interfaces live under
io.reactivex.rxjava4.core. If you are migrating from RxJava 3, the package prefix changed fromio.reactivex.rxjava3toio.reactivex.rxjava4.
1.3 A more realistic example
Run an expensive computation on a background thread and observe the result on another thread:
import io.reactivex.rxjava4.core.Flowable;
import io.reactivex.rxjava4.schedulers.Schedulers;
Flowable.fromCallable(() -> {
Thread.sleep(1000); // imitate expensive work
return "Done";
})
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.single())
.subscribe(System.out::println, Throwable::printStackTrace);
Thread.sleep(2000); // wait for the async flow to finish
Done
Warning
RxJava's standard schedulers run on daemon threads. Once the Java
mainthread exits, those threads stop and pending background work may never run. In short examples like the one above, the trailingThread.sleep(...)keeps the JVM alive long enough to see the output. In a real application (a server, an Android app, a GUI), the application lifecycle keeps the JVM running, so the sleep is unnecessary.
2. Installation
2.1 Requirements
| Requirement | Detail |
|---|---|
| Java runtime | Java 26 (RxJava 4 is a native Java 26 build; the build toolchain targets JDK 26) |
| Runtime dependencies | None — RxJava 4 is self-contained in a single JAR |
| Module system | JPMS (module-info) and OSGi metadata are provided in the JAR |
| Android | Supported subject to your API level and available desugaring |
Note
RxJava 3 required Java 8+ (or Android API 21+). RxJava 4 raises the baseline to Java 26 to take advantage of virtual threads, the
java.util.concurrent.Flowtypes, records, and sealed interfaces. If you cannot run Java 26 yet, stay on RxJava 3 (io.reactivex.rxjava3:rxjava), which is supported for at least one year after the 4.x official release.
2.2 Released binaries
Released versions are available from Maven Central. Use the dependency snippets in Quick Start.
2.3 Snapshots
Pre-release snapshots are published to the Sonatype snapshots repository:
repositories {
maven { url 'https://central.sonatype.com/repository/maven-snapshots' }
}
dependencies {
implementation 'io.reactivex.rxjava4:rxjava:4.0.0-SNAPSHOT'
}
2.4 R8 / ProGuard (Android)
By default RxJava itself needs no ProGuard/R8 rules. However, the Reactive Streams metadata embedded in dependencies can trigger ProGuard warnings about java.util.concurrent.Flow. If you see them, add:
-dontwarn java.util.concurrent.Flow*
For R8, the RxJava JAR bundles META-INF/proguard/rxjava4.pro with the same no-warning clause, which is applied automatically.
3. Core Concepts
RxJava revolves around a small number of ideas. Understanding them makes every operator in the library easier to reason about.
3.1 The five base reactive types
RxJava 4 offers five base classes. You pick one based on how many items the source can produce and whether you need backpressure (flow control).
| Type | Cardinality | Backpressure | Use it for |
|---|---|---|---|
Flowable<T> |
0..N items | Yes | Large or unbounded streams where the producer can outpace the consumer (file/DB reads, network streams) |
Observable<T> |
0..N items | No | Short sequences, GUI/UI events, where overwhelming the consumer isn't a concern |
Single<T> |
exactly 1 item or error | n/a | A single async result (the lazy equivalent of a Future) |
Maybe<T> |
0 or 1 item or error | n/a | An optional async result (may complete empty) |
Completable |
0 items — completion or error | n/a | Async work with no result, only success/failure (e.g. "write to DB") |
All five live in io.reactivex.rxjava4.core:
import io.reactivex.rxjava4.core.Flowable;
import io.reactivex.rxjava4.core.Observable;
import io.reactivex.rxjava4.core.Single;
import io.reactivex.rxjava4.core.Maybe;
import io.reactivex.rxjava4.core.Completable;
Tip
When in doubt between
FlowableandObservable: useFlowableif the source could emit a large or unknown number of items, or if any step in the pipeline is asynchronous and could fall behind. UseObservablefor bounded, in-memory, or UI-event streams.Single,Maybe, andCompletablenever need backpressure — there is always room to hold at most one item.
3.2 Upstream and downstream
A flow is a source, zero or more intermediate operators, and a consumer:
source
.operator1()
.operator2()
.operator3()
.subscribe(consumer);
Standing on any operator: everything toward the source (left/up) is upstream; everything toward the consumer (right/down) is downstream.
3.3 Objects in motion
In RxJava documentation, the words emission, emits, item, event, signal, data, and message are synonyms — they all refer to the object travelling along the dataflow.
3.4 Assembly time vs. subscription time vs. runtime
This distinction explains a large class of bugs (see the deferred-evaluation pitfall).
-
Assembly time — when you chain operators together. No data flows yet; no side effects happen.
Flowable<Integer> flow = Flowable.range(1, 5) .map(v -> v * v) .filter(v -> v % 3 == 0); // nothing has run yet -
Subscription time — when you call
subscribe(...). The processing chain is wired up internally;doOnSubscribeside effects fire; some sources begin emitting. -
Runtime — when the flow is actively emitting items, errors, or a completion signal to the consumer.
3.5 Hot vs. cold sources
| Term | Meaning |
|---|---|
| Cold | Has side effects when subscribed. Each subscription starts fresh (e.g. an HTTP call runs again per subscriber). |
| Hot | Does not cause side effects on subscription — it is already producing (e.g. mouse events, a Subject). Late subscribers may miss earlier emissions. |
3.6 Immutability and the fluent API
Operator chaining looks like the builder pattern, but RxJava's reactive types are immutable: each operator call returns a new instance with the added behavior. The original is unchanged.
Flowable<String> source = Flowable.fromCallable(() -> compute());
Flowable<String> runBackground = source.subscribeOn(Schedulers.computation());
Flowable<String> showResult = runBackground.observeOn(Schedulers.single());
showResult.subscribe(System.out::println);
4. Creating Flows
Every base type exposes a family of static factory methods. The names below are the same across Flowable, Observable, Single, Maybe, and Completable where they make sense.
4.1 From known values
| Factory | Emits |
|---|---|
just(item) / just(a, b, ...) |
The given constant item(s), then completes |
fromArray(T...) |
Each element of the array |
fromIterable(Iterable<T>) |
Each element of the iterable |
range(start, count) |
A sequence of count integers starting at start |
empty() |
No items, immediate completion |
never() |
No items and never terminates |
error(Throwable) / error(Supplier) |
Terminates immediately with the given error |
Flowable.just(1, 2, 3).subscribe(System.out::println);
Flowable.range(1, 5).subscribe(System.out::println);
Flowable.fromIterable(List.of("a", "b", "c")).subscribe(System.out::println);
4.2 From a computation or external call
| Factory | Behavior |
|---|---|
fromCallable(Callable<T>) |
Runs the callable lazily on each subscription; emits its result |
fromSupplier(Supplier<T>) |
Like fromCallable but for a non-throwing supplier |
fromFuture(Future<T>) |
Bridges a java.util.concurrent.Future into the flow |
defer(Supplier<Source>) |
Defers building the source until subscription time — a fresh source per subscriber |
Flowable<String> lazy = Flowable.fromCallable(() -> expensiveCall());
// expensiveCall() runs only when someone subscribes — and once per subscription
4.3 From custom producers
create(...) lets you bridge an arbitrary push-based API (callbacks, listeners) into a flow. Because Flowable supports backpressure, its emitter requires a BackpressureStrategy:
Observable.create(emitter -> {
while (!emitter.isDisposed()) {
long time = System.currentTimeMillis();
emitter.onNext(time);
if (time % 2 != 0) {
emitter.onError(new IllegalStateException("Odd millisecond!"));
break;
}
}
})
.subscribe(System.out::println, Throwable::printStackTrace);
generate(...) is a safe, synchronous, backpressure-aware generator that produces items one at a time on demand — preferable to create when you can produce values lazily.
4.4 From time
| Factory | Behavior |
|---|---|
interval(period, unit) |
Emits an incrementing Long periodically |
timer(delay, unit) |
Emits a single 0L after a delay, then completes |
Flowable.interval(1, java.util.concurrent.TimeUnit.SECONDS)
.take(5)
.subscribe(System.out::println);
4.5 From other reactive libraries
Flowable.fromPublisher(Publisher) wraps any Reactive Streams / java.util.concurrent.Flow.Publisher into a Flowable, enabling interop with other reactive libraries.
5. Transforming and Combining Flows
This is the heart of RxJava. Operators are instance methods that return a new flow.
5.1 The everyday operators
| Operator | What it does |
|---|---|
map(Function) |
Transform each item 1:1 |
filter(Predicate) |
Keep only items matching the predicate |
flatMap(Function) |
Map each item to an inner flow and merge the results (order not guaranteed) |
concatMap(Function) |
Like flatMap but runs inner flows one at a time, in order |
concatMapEager(Function) |
Runs inner flows concurrently but emits results in source order |
switchMap(Function) |
Map to an inner flow, cancelling the previous inner flow on each new item |
scan(BiFunction) |
Emit running accumulations (each intermediate fold) |
reduce(BiFunction) |
Emit a single folded result (returns a Maybe/Single) |
buffer(...) |
Group items into Lists by count or time |
window(...) |
Like buffer but emits inner flows instead of lists |
groupBy(Function) |
Split one flow into keyed sub-flows |
distinct() / distinctUntilChanged() |
Drop duplicates / consecutive duplicates |
take(n) / skip(n) |
Keep the first n / drop the first n |
debounce(...) / throttleLatest(...) |
Rate-limit bursty sources |
Flowable.range(1, 10)
.map(v -> v * v)
.filter(v -> v % 2 == 0)
.subscribe(System.out::println);
4
16
36
64
100
5.2 Defer evaluation until runtime
A common pitfall: code captured at assembly time runs too early.
AtomicInteger count = new AtomicInteger();
Observable.range(1, 10)
.doOnNext(ignored -> count.incrementAndGet())
.ignoreElements() // -> Completable
.andThen(Single.just(count.get())) // BUG: count.get() read at assembly time
.subscribe(System.out::println);
0
Single.just(count.get()) evaluates count.get() immediately when the chain is assembled, before any data flows. Defer it to runtime with defer or fromCallable:
Observable.range(1, 10)
.doOnNext(ignored -> count.incrementAndGet())
.ignoreElements()
.andThen(Single.defer(() -> Single.just(count.get()))) // evaluated at runtime
.subscribe(System.out::println);
10
Caution
Any value you compute and pass into a factory method (
just,error(new ...), etc.) is evaluated at assembly time. If the value must reflect state that only exists once the flow runs, wrap it indefer(...)orfromCallable(...).
5.3 Continuations (chaining dependent calls)
Call one service, then another with the first result:
service.apiCall()
.flatMap(value -> service.anotherApiCall(value))
.flatMap(next -> service.finalCall(next))
.subscribe(...);
When a later step needs an earlier value, nest the mapping to capture it via the lambda:
service.apiCall()
.flatMap(value ->
service.anotherApiCall(value)
.flatMap(next -> service.finalCallBoth(value, next)));
When the next step is independent of the previous result, use Completable.andThen(...):
sourceObservable
.ignoreElements() // -> Completable
.andThen(someSingleSource)
.map(v -> v.toString());
5.4 Dependent sub-flows
flatMap shines when each item fans out into another asynchronous call:
Flowable<Inventory> inventory = warehouse.getInventoryAsync();
inventory
.flatMap(item -> erp.getDemandAsync(item.getId())
.map(demand -> "Item " + item.getName() + " has demand " + demand))
.subscribe(System.out::println);
6. Concurrency and Schedulers
RxJava operators never touch Thread or ExecutorService directly. Instead they work through Schedulers, which abstract a source of concurrency behind one uniform API.
6.1 The standard schedulers
Access them via the io.reactivex.rxjava4.schedulers.Schedulers utility class.
| Scheduler | Backed by | Use for |
|---|---|---|
Schedulers.computation() |
Fixed pool sized to available CPUs | CPU-bound work, event loops, processing callbacks (the default for most async operators) |
Schedulers.cached() |
Dynamically growing pool of reusable single-thread executors | Blocking I/O-bound work (new in 4.x; the replacement for io()) |
Schedulers.virtual() |
One Executors.newVirtualThreadPerTaskExecutor() per worker |
Virtual-blocking and many-IO workloads on virtual threads (new in 4.x) |
Schedulers.single() |
One shared background thread (FIFO, sequential) | Strongly-sequential work; returning to a single dedicated thread |
Schedulers.newThread() |
A brand-new thread per unit of work | Rare; prefer the pooled schedulers |
Schedulers.trampoline() |
The current thread, queueing work FIFO | Testing; recursive scheduling without new threads |
import io.reactivex.rxjava4.schedulers.Schedulers;
Warning
Schedulers.io()is deprecated since 4.0.0 and now simply delegates toSchedulers.cached(). Migrate I/O-bound work toSchedulers.cached()(classic blocking pool) orSchedulers.virtual()(virtual threads). Bothcached()andnewThread()can create an unbounded number of threads under load — bound your concurrency (e.g. withflatMap'smaxConcurrency) to avoidOutOfMemoryError.
6.2 subscribeOn vs. observeOn
These two operators are the levers you use to move work between threads.
| Operator | Controls | Notes |
|---|---|---|
subscribeOn(scheduler) |
The thread where the source starts (subscription side effects + emission origin) | Only the first subscribeOn in a chain takes effect |
observeOn(scheduler) |
The thread used for everything downstream of the call | Can appear multiple times to hop threads repeatedly |
Flowable.fromCallable(() -> blockingRead())
.subscribeOn(Schedulers.cached()) // do the blocking read on a cached I/O thread
.observeOn(Schedulers.computation()) // process results on a computation thread
.map(this::parse)
.subscribe(this::render);
6.3 Wrapping your own executor
ExecutorService exec = Executors.newSingleThreadExecutor();
try {
Scheduler scheduler = Schedulers.from(exec);
Flowable.just(1)
.subscribeOn(scheduler)
.map(v -> v + 1)
.observeOn(scheduler)
.blockingSubscribe(System.out::println);
} finally {
exec.shutdown();
}
Overloads let you opt into task interruption and fairness:
Schedulers.from(Executor)— basic wrapper; tasks are not interruptible.Schedulers.from(Executor, interruptibleWorker)— disposing a task interrupts it.Schedulers.from(Executor, interruptibleWorker, fair)—fair=truereschedules between tasks so other users of the executor can interleave.Schedulers.fromSupplier(Supplier<Executor>, interruptible, fair)— each worker gets its own executor.
Important
The wrapped
Executorshould not throwRejectedExecutionException(don't shut it down prematurely or use a bounded-queue executor). If it does, the exception is routed to the global error handler viaRxJavaPlugins.onError(...). Cancel/dispose all flows before shutting down the underlying executor.
6.4 Concurrency within a flow
A flow is sequential by nature, but its stages can run concurrently:
Flowable.range(1, 10)
.observeOn(Schedulers.computation())
.map(v -> v * v)
.blockingSubscribe(System.out::println);
Here squaring happens on a computation thread and consumption on the caller thread — but v -> v * v still processes items one at a time, not in parallel.
6.5 True parallel processing
To process items in parallel, run independent flows and merge them. Two idioms:
flatMap fan-out (results may interleave):
Flowable.range(1, 10)
.flatMap(v ->
Flowable.just(v)
.subscribeOn(Schedulers.computation())
.map(w -> w * w))
.blockingSubscribe(System.out::println);
ParallelFlowable via the parallel() operator:
Flowable.range(1, 10)
.parallel()
.runOn(Schedulers.computation())
.map(v -> v * v)
.sequential()
.blockingSubscribe(System.out::println);
7. Backpressure (Flowable only)
Backpressure is flow control: a consumer signals how many items it is ready to receive, so a fast producer can't overwhelm it (which would otherwise show up as unbounded buffering, memory growth, or dropped data). Only Flowable supports backpressure; Observable does not.
7.1 Converting an Observable into a Flowable
When you turn an unbounded Observable into a backpressured Flowable, you must decide what to do with overflow by supplying a BackpressureStrategy:
observable.toFlowable(BackpressureStrategy.BUFFER);
BackpressureStrategy |
Behavior when downstream can't keep up |
|---|---|
MISSING |
Emit without buffering/dropping; downstream must handle overflow (often paired with an onBackpressureXXX operator) |
ERROR |
Signal MissingBackpressureException |
BUFFER |
Buffer all items until the downstream consumes them |
DROP |
Drop the most recent item that can't be delivered |
LATEST |
Keep only the latest item, overwriting earlier undelivered ones |
7.2 The onBackpressureXXX operators
For finer control on an existing Flowable:
onBackpressureBuffer(...)— buffer overflow (many overloads, including bounded capacity + aBackpressureOverflowStrategy).onBackpressureDrop(...)— drop items the downstream isn't ready for.onBackpressureLatest()— keep only the latest undelivered item.
When using a bounded buffer, choose what happens when the buffer fills via BackpressureOverflowStrategy:
BackpressureOverflowStrategy |
Behavior |
|---|---|
ERROR |
Signal MissingBackpressureException and terminate |
DROP_OLDEST |
Discard the oldest buffered value |
DROP_LATEST |
Discard the newest value |
Tip
The default internal buffer size is 128 items. You can override it globally before the
Flowableclass loads via the system propertyrx4.buffer-size, or read it withFlowable.bufferSize().
8. Consuming Flows
8.1 subscribe overloads
flow.subscribe(); // fire-and-forget
flow.subscribe(onNext); // values only
flow.subscribe(onNext, onError); // values + error
flow.subscribe(onNext, onError, onComplete); // values + error + completion
Caution
If you subscribe without an
onErrorhandler and the flow errors, RxJava wraps the error in anOnErrorNotImplementedExceptionand routes it to the global error handler. Always provide anonErrorhandler in production code.
8.2 Blocking consumption
For tests, main methods, and CLI tools, the blocking* family runs the flow on the caller thread until it terminates:
flow.blockingSubscribe(System.out::println);
List<T> all = flow.toList().blockingGet();
T first = flow.blockingFirst();
Note
Avoid blocking operators inside reactive pipelines or on the
computation()/single()schedulers. IfRxJavaPlugins.setFailOnNonBlockingScheduler(true)is set, attempting to block on those schedulers throwsIllegalStateException.
8.3 Converting between types
Each base type can convert to the others. The most common conversions:
| From → To | Operator(s) |
|---|---|
Flowable/Observable → Single |
first, firstOrError, single, singleOrError, last, lastOrError |
Flowable/Observable → Maybe |
firstElement, singleElement, lastElement |
Flowable/Observable → Completable |
ignoreElements |
Observable → Flowable |
toFlowable(BackpressureStrategy) |
Single/Maybe/Completable → Flowable/Observable |
toFlowable, toObservable |
Single ↔ Maybe |
toMaybe, toSingle |
When an operator must accept a different reactive type, RxJava uses type-suffixed overloads (because Java type erasure forbids identically-named overloads):
| Base operator | Suffixed variants |
|---|---|
flatMap |
flatMapSingle, flatMapMaybe, flatMapCompletable, flatMapIterable |
concatMap |
concatMapSingle, concatMapMaybe, concatMapCompletable, concatMapIterable |
switchMap |
switchMapSingle, switchMapMaybe, switchMapCompletable |
9. Virtual threads and Streamable (new in 4.x)
RxJava 4 introduces a virtual-thread-native way of building flows. Where classic operators avoid blocking at all costs, the virtual-thread APIs let your generator code block freely — virtual threads make blocking cheap — while still participating in backpressure.
Note
These APIs are new in 4.x and still being finalized (several carry "TODO proper docs" notes in their source). Treat them as evolving. The classic reactive types remain the recommended foundation for most code today.
9.1 Flowable.virtualCreate
Generate a backpressured Flowable from a callback that is free to block:
Flowable<Integer> flow = Flowable.virtualCreate(emitter -> {
for (int i = 1; i <= 5; i++) {
emitter.emit(i); // blocks until the downstream requests more
}
// returning normally signals onComplete
});
- Return normally from the generator to signal
onComplete. - Throw any exception to signal
onError. - By default the generator runs on
Schedulers.virtual(). Overloads accept a specificSchedulerorExecutorService:virtualCreate(VirtualGenerator)virtualCreate(VirtualGenerator, Scheduler)virtualCreate(VirtualGenerator, ExecutorService)
9.2 virtualTransform
A virtual-thread analogue of flatMap/concatMap — for each upstream item your callback may emit zero or more downstream items and may block:
flow.virtualTransform((value, emitter, stopper) -> {
emitter.emit(value);
emitter.emit(value * 10);
// call stopper.dispose() to stop the upstream early
});
9.3 Streamable — the async-enumerable of the Java world
Streamable<T> (with its realized counterpart Streamer<T>) is a pull-style async sequence — conceptually like C#'s IAsyncEnumerable/IAsyncEnumerator, built to run on virtual threads.
import io.reactivex.rxjava4.core.Streamable;
Streamable<Integer> s = Streamable.create(emitter -> {
emitter.emit(1);
emitter.emit(2);
emitter.emit(3);
});
s.forEach(System.out::println); // consume on a virtual-thread executor
Useful entry points:
| Method | Purpose |
|---|---|
Streamable.empty() / Streamable.just(item) |
Trivial sources |
Streamable.create(VirtualGenerator [, Scheduler / ExecutorService]) |
Generate values from a (blocking-friendly) callback |
Streamable.fromPublisher(Flow.Publisher [, ExecutorService]) |
Bridge any Reactive Streams publisher |
streamable.transform(VirtualTransformer) |
Per-item virtual transform |
streamable.toFlowable([ExecutorService]) |
Convert to a backpressured Flowable |
flowable.toStreamable([ExecutorService]) |
Convert a Flowable to a Streamable |
streamable.forEach(consumer) / subscribe(Flow.Subscriber) / test() |
Consume the sequence |
10. Error Handling
Errors travel downstream as onError signals. RxJava provides operators to recover, retry, or transform them.
| Operator | Behavior |
|---|---|
onErrorReturn(Function) / onErrorReturnItem(item) |
Replace an error with a fallback value, then complete |
onErrorResumeNext(Function) |
Switch to a fallback flow when an error occurs |
onErrorComplete() |
Swallow the error and complete normally |
retry() / retry(n) / retry(Predicate) |
Re-subscribe on error (optionally limited or conditional) |
retryWhen(Function) |
Drive retries from a secondary signal flow (e.g. exponential backoff) |
doOnError(Consumer) |
Side effect on error without consuming it |
timeout(...) |
Emit a TimeoutException (or switch to a fallback) if the source is too slow |
service.apiCall()
.timeout(5, TimeUnit.SECONDS)
.retry(3)
.onErrorReturnItem(Collections.emptyList())
.subscribe(this::render, this::logError);
10.1 Delaying errors across multiple sources
When combining several sources, many operators have a DelayError variant (or a delayError/delayErrors boolean) that waits for all sources to finish before propagating any error, instead of failing fast:
Flowable<T> result = Flowable.concatDelayError(sources);
Suffixes can stack, e.g. concatArrayEagerDelayError(...).
10.2 RxJava's exception types
| Exception | Meaning |
|---|---|
MissingBackpressureException |
The downstream wasn't ready and didn't request(...) in time. Default message: "Could not emit value due to lack of requests" |
QueueOverflowException |
Upstream ignored backpressure or called onNext concurrently without synchronization. Message: "Queue overflow due to illegal concurrent onNext calls or a bug in an operator" |
OnErrorNotImplementedException |
You subscribed without an onError handler and the flow errored |
UndeliverableException |
An error had nowhere to go (the flow was already disposed/terminated) and was routed to the global handler |
CompositeException |
A container of multiple errors (e.g. from mergeDelayError) |
ProtocolViolationException |
An internal Reactive Streams protocol violation |
The io.reactivex.rxjava4.exceptions.Exceptions utility offers Exceptions.propagate(throwable) (rethrow as unchecked) and Exceptions.throwIfFatal(throwable) (rethrow only truly fatal errors such as VirtualMachineError/LinkageError).
11. Best Practices
- [ ] Pick the narrowest base type. Use
Single/Maybe/Completablewhen the cardinality is known — they're simpler and need no backpressure. - [ ] Always supply an
onErrorhandler insubscribe(...). Unhandled errors becomeOnErrorNotImplementedException. - [ ] Use
Flowable(notObservable) for large or asynchronous streams so backpressure protects you fromOutOfMemoryError. - [ ] Bound concurrency.
flatMapaccepts amaxConcurrency; theFlatMapConfigrecord bundles it withdelayErrorsandbufferSize. - [ ] Defer state-dependent values with
defer(...)/fromCallable(...)so they evaluate at runtime, not assembly time. - [ ] Match the scheduler to the work:
computation()for CPU work,cached()(orvirtual()) for blocking I/O,single()for sequential work. Never block oncomputation()/single(). - [ ] Dispose subscriptions tied to a lifecycle (UI screens, request scopes) to release resources — keep the returned
Disposableand calldispose()(or collect them in aCompositeDisposable). - [ ] Treat
io.reactivex.rxjava4.internal.*as off-limits — it is private API and can change at any time.
Tip
Operator names follow consistent conventions. A suffix tells you the variant: a target type (
flatMapSingle), error handling (...DelayError), or array input (mergeArray). Once you learn the conventions, you can usually guess the operator you need.
12. Troubleshooting
Error: Could not emit value due to lack of requests
Cause: A MissingBackpressureException — your Flowable produced items faster than the downstream requested them, with no buffering strategy in place (or a bounded buffer using BackpressureStrategy.ERROR).
Solution: Apply a backpressure strategy: onBackpressureBuffer(), onBackpressureLatest(), or onBackpressureDrop(). If converting from Observable, pass BackpressureStrategy.BUFFER/LATEST/DROP to toFlowable(...).
Prevention: Prefer backpressure-aware sources (range, fromIterable, generate) over create(...) with MISSING, and use Flowable rather than Observable for fast or unbounded producers.
Error: Queue overflow due to illegal concurrent onNext calls or a bug in an operator
Cause: A QueueOverflowException — an upstream emitted onNext concurrently from multiple threads without serialization, or completely ignored backpressure.
Solution: Serialize emissions. If you write to an emitter from multiple threads, use a Subject/Processor's toSerialized() form, or restructure so a single thread emits.
Prevention: Never call onNext from multiple threads simultaneously. Follow the Reactive Streams contract: onSubscribe onNext* (onError | onComplete)?.
Error: OnErrorNotImplementedException
Cause: You called a subscribe(...) overload without an onError handler, and the flow emitted an error.
Solution: Add an onError consumer: subscribe(onNext, onError).
Prevention: Make providing onError a code-review rule; consider a global handler via RxJavaPlugins.setErrorHandler(...) as a safety net (not a substitute).
Problem: The program exits before any output appears
Cause: RxJava's standard schedulers run on daemon threads. When main returns, the JVM exits and kills them before async work completes.
Solution: Keep the JVM alive — blockingSubscribe(...), blockingGet(), a CountDownLatch, or (in examples) a trailing Thread.sleep(...).
Prevention: In long-running apps (servers, Android, GUIs) the application lifecycle keeps the JVM alive; this only bites in short main methods and tests.
Problem: A computed value is wrong / shows a stale value (e.g. prints 0)
Cause: The value was read at assembly time instead of runtime — e.g. Single.just(counter.get()) captures the counter when the chain is built.
Solution: Wrap it in Single.defer(() -> Single.just(counter.get())) or Single.fromCallable(() -> counter.get()).
Prevention: Remember: arguments passed into factory methods are evaluated immediately. Defer anything that depends on the flow having already run.
Error: IllegalStateException when blocking on a scheduler
Cause: RxJavaPlugins.setFailOnNonBlockingScheduler(true) is enabled and you attempted a blocking operation on computation() or single().
Solution: Move blocking work to cached()/virtual() via subscribeOn(...), or restructure to avoid blocking.
Prevention: Keep blocking calls off the computation/single schedulers as a matter of course.
Warning: ProGuard can't find ... java.util.concurrent.Flow$Publisher
Cause: Reactive Streams metadata references java.util.concurrent.Flow types that plain ProGuard can't resolve.
Solution: Add -dontwarn java.util.concurrent.Flow* to your ProGuard rules. R8 picks up the bundled rxjava4.pro automatically.
Prevention: Use R8 (default on modern Android), which applies the consumer rules shipped in the JAR.
13. FAQ
Q: What's the difference between Flowable and Observable?
Both model 0..N item streams. Flowable supports backpressure (the consumer can throttle the producer) and implements Reactive Streams Publisher; Observable does not. Use Flowable for large/unbounded/async streams and Observable for short or UI-event streams.
Q: When should I use Single, Maybe, or Completable?
Single for exactly one result-or-error, Maybe for zero-or-one result-or-error, Completable for work that only succeeds or fails with no value. They're simpler than Flowable/Observable and never need backpressure.
Q: How do I run work in the background and get results on another thread?
subscribeOn(scheduler) chooses where the source runs; observeOn(scheduler) chooses where downstream operators run. Typical pattern: .subscribeOn(Schedulers.cached()).observeOn(Schedulers.computation()).
Q: Schedulers.io() shows as deprecated — what do I use?
io() is deprecated since 4.0.0 and delegates to cached(). Use Schedulers.cached() for classic blocking I/O, or Schedulers.virtual() to run blocking work on virtual threads.
Q: What are virtual threads good for here?
Schedulers.virtual() and the Streamable/virtualCreate APIs let your generator/consumer code block freely (e.g. blocking I/O) cheaply, because each task gets a lightweight virtual thread. This is new in 4.x and well-suited to many-IO workloads.
Q: Why are some operators named with suffixes like flatMapSingle or mergeArray?
Java type erasure makes flatMap(Function<T, Single<R>>) and flatMap(Function<T, Maybe<R>>) indistinguishable, so RxJava appends the target type as a suffix. Array-input variants get an Array suffix to avoid varargs ambiguity.
Q: My lambda passed to concatWith(...) doesn't work — why?
Several concatWith overloads accept functional-interface types (Publisher, SingleSource), so a lambda is ambiguous and won't compile. You almost certainly wanted defer(...): someSource.concatWith(Single.defer(() -> Single.just(2))).
Q: Is RxJava 4 a drop-in upgrade from RxJava 3?
No. RxJava 4 changes the package prefix to io.reactivex.rxjava4, raises the minimum JVM to Java 26, deprecates io(), renames system properties to the rx4.* prefix, and adds new virtual-thread APIs. Plan a migration rather than a version bump. RxJava 3 remains supported for at least a year after the 4.x release.
Q: Can RxJava interoperate with other reactive libraries?
Yes. Flowable implements Reactive Streams Publisher, and Flowable.fromPublisher(...) / Streamable.fromPublisher(...) bridge any java.util.concurrent.Flow.Publisher into RxJava.
Q: What stability guarantees do APIs carry?
APIs marked @Beta may change or be removed; @Experimental APIs almost certainly will and shouldn't be used in production; @Deprecated APIs remain until the next major release. Anything in io.reactivex.rxjava4.internal.* is private and unsupported.
Q: Where can I find the complete operator reference?
The Javadoc at http://reactivex.io/RxJava/4.x/javadoc/ documents every operator and overload on each base class. The project Wiki covers concepts and migration.