ReactiveX/RxJava · error · NullPointerException
Operator ${operator} returned a null Subscriber
Error message
Operator ${operator} returned a null Subscriber What it means
Thrown during subscription when a user-supplied FlowableOperator (passed to Flowable.lift() or used internally by operators like compose/transform) returned null from its apply(Subscriber) method. RxJava treats a null Subscriber as a contract violation because the Reactive-Streams spec requires a real Subscriber to receive signals; it surfaces this as a NullPointerException rather than forwarding null downstream.
Source
Thrown at src/main/java/io/reactivex/rxjava4/internal/operators/flowable/FlowableLift.java:46
* @param <T> the upstream value type
* @param <R> the downstream parameter type
*/
public final class FlowableLift<R, T> extends AbstractFlowableWithUpstream<T, R> {
/** The actual operator. */
final FlowableOperator<? extends R, ? super T> operator;
public FlowableLift(Flowable<T> source, FlowableOperator<? extends R, ? super T> operator) {
super(source);
this.operator = operator;
}
@Override
public void subscribeActual(Subscriber<? super R> s) {
try {
Subscriber<? super T> st = operator.apply(s);
if (st == null) {
throw new NullPointerException("Operator " + operator + " returned a null Subscriber");
}
source.subscribe(st);
} catch (NullPointerException e) { // NOPMD
throw e;
} catch (Throwable e) {
Exceptions.throwIfFatal(e);
// can't call onError because no way to know if a Subscription has been set or not
// can't call onSubscribe because the call might have set a Subscription already
RxJavaPlugins.onError(e);
NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS");
npe.initCause(e);
throw npe;
}
}
}
View on GitHub (pinned to a8ab535614)
Solutions
- Ensure FlowableOperator.apply() never returns null on any path; always return a valid Subscriber (even a no-op like a pass-through).
- If the operator must be conditional, return the downstream Subscriber unchanged rather than null when bypassing.
- Add unit tests asserting apply() returns non-null for every input branch.
- Use Operators.serialize or a wrapper Subscriber as the default fallback instead of null.
Example fix
// before
FlowableOperator<Integer, Integer> op = child -> {
if (!enabled) return null; // -> NPE on subscribe
return new MapSubscriber<>(child, x -> x + 1);
};
// after
FlowableOperator<Integer, Integer> op = child -> {
if (!enabled) return child; // pass-through, never null
return new MapSubscriber<>(child, x -> x + 1);
}; Defensive patterns
Strategy: validation
Validate before calling
// Validate a custom FlowableOperator never returns null before use.
FlowableOperator<R, T> op = /* your operator */;
FlowableOperator<R, T> safeOp = child -> {
Subscriber<? super T> s = op.apply(child);
if (s == null) {
throw new NullPointerException("Operator " + op + " returned null Subscriber");
}
return s;
};
// Better: unit-test every branch of apply() for non-null output before wiring it in. Type guard
static <T, R> boolean operatorNeverReturnsNull(FlowableOperator<R, T> op) {
// Best-effort static check: inspect that no 'return null;' literal exists;
// runtime guarantee requires a test subscriber probe per branch.
return op != null; // a real guarantee needs branch-coverage tests, see validationCode
} Try / catch
try {
flowable.lift(op).subscribe(sub);
} catch (NullPointerException e) {
if (e.getMessage() != null && e.getMessage().contains("returned a null Subscriber")) {
// custom operator returned null; fix apply() to return a real Subscriber
log.error("Custom operator returned null Subscriber: " + op, e);
}
throw e;
} Prevention
- Never return null from FlowableOperator.apply(); return the downstream child as a pass-through when bypassing.
- Add unit tests covering every branch of apply() to assert a non-null Subscriber.
- Wrap custom operators with a null-checking decorator during development.
- Prefer built-in operators (map, filter, compose) over hand-written lift where possible.
When it happens
Trigger: A custom FlowableOperator whose apply() has a code path that returns null (forgotten return, early-out, exception swallowed); an operator that returns null intentionally 'to skip'; a lambda-based operator (FlowableOperator as a lambda) where the lambda body yields null.
Common situations: Hand-written lift operator with incomplete branching; porting an RxJava 2/3 operator that previously returned null in some path; a refactor that deleted the return statement; conditional operators that return null when a feature flag is off.
Related errors
- count >= 0 required but it was {}
- Overflow! start + count is bigger than Long.MAX_VALUE
- remove
- Read-only iterator.
- Read only iterator
AI-assisted analysis of ReactiveX/RxJava@a8ab535614 (2026-08-13).
Data as JSON: /api/errors/056eba6dc9114a7c.
Report an issue: GitHub.