Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

RxJava 4 User Guide

Generated June 2026 from ReactiveX/RxJava at 6beb0e3

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 and Streamable.


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 from io.reactivex.rxjava3 to io.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 main thread exits, those threads stop and pending background work may never run. In short examples like the one above, the trailing Thread.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.Flow types, 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 Flowable and Observable: use Flowable if the source could emit a large or unknown number of items, or if any step in the pipeline is asynchronous and could fall behind. Use Observable for bounded, in-memory, or UI-event streams. Single, Maybe, and Completable never 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; doOnSubscribe side 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 in defer(...) or fromCallable(...).

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 to Schedulers.cached(). Migrate I/O-bound work to Schedulers.cached() (classic blocking pool) or Schedulers.virtual() (virtual threads). Both cached() and newThread() can create an unbounded number of threads under load — bound your concurrency (e.g. with flatMap's maxConcurrency) to avoid OutOfMemoryError.

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=true reschedules 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 Executor should not throw RejectedExecutionException (don't shut it down prematurely or use a bounded-queue executor). If it does, the exception is routed to the global error handler via RxJavaPlugins.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 + a BackpressureOverflowStrategy).
  • 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 Flowable class loads via the system property rx4.buffer-size, or read it with Flowable.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 onError handler and the flow errors, RxJava wraps the error in an OnErrorNotImplementedException and routes it to the global error handler. Always provide an onError handler 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. If RxJavaPlugins.setFailOnNonBlockingScheduler(true) is set, attempting to block on those schedulers throws IllegalStateException.

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 specific Scheduler or ExecutorService:
    • 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/Completable when the cardinality is known — they're simpler and need no backpressure.
  • [ ] Always supply an onError handler in subscribe(...). Unhandled errors become OnErrorNotImplementedException.
  • [ ] Use Flowable (not Observable) for large or asynchronous streams so backpressure protects you from OutOfMemoryError.
  • [ ] Bound concurrency. flatMap accepts a maxConcurrency; the FlatMapConfig record bundles it with delayErrors and bufferSize.
  • [ ] 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() (or virtual()) for blocking I/O, single() for sequential work. Never block on computation()/single().
  • [ ] Dispose subscriptions tied to a lifecycle (UI screens, request scopes) to release resources — keep the returned Disposable and call dispose() (or collect them in a CompositeDisposable).
  • [ ] 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.