{"record":{"id":"056eba6dc9114a7c","repo":"ReactiveX/RxJava","slug":"operator-operator-returned-a-null-subscriber","errorCode":null,"errorMessage":"Operator ${operator} returned a null Subscriber","messagePattern":"Operator (.+?) returned a null Subscriber","errorType":"exception","errorClass":"NullPointerException","httpStatus":null,"severity":"error","filePath":"src/main/java/io/reactivex/rxjava4/internal/operators/flowable/FlowableLift.java","lineNumber":46,"sourceCode":" * @param <T> the upstream value type\n * @param <R> the downstream parameter type\n */\npublic final class FlowableLift<R, T> extends AbstractFlowableWithUpstream<T, R> {\n    /** The actual operator. */\n    final FlowableOperator<? extends R, ? super T> operator;\n\n    public FlowableLift(Flowable<T> source, FlowableOperator<? extends R, ? super T> operator) {\n        super(source);\n        this.operator = operator;\n    }\n\n    @Override\n    public void subscribeActual(Subscriber<? super R> s) {\n        try {\n            Subscriber<? super T> st = operator.apply(s);\n\n            if (st == null) {\n                throw new NullPointerException(\"Operator \" + operator + \" returned a null Subscriber\");\n            }\n\n            source.subscribe(st);\n        } catch (NullPointerException e) { // NOPMD\n            throw e;\n        } catch (Throwable e) {\n            Exceptions.throwIfFatal(e);\n            // can't call onError because no way to know if a Subscription has been set or not\n            // can't call onSubscribe because the call might have set a Subscription already\n            RxJavaPlugins.onError(e);\n\n            NullPointerException npe = new NullPointerException(\"Actually not, but can't throw other exceptions due to RS\");\n            npe.initCause(e);\n            throw npe;\n        }\n    }\n}\n","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/ReactiveX/RxJava/blob/a8ab5356143f37a8f1dd6d76c79191bec5fa343b/src/main/java/io/reactivex/rxjava4/internal/operators/flowable/FlowableLift.java#L28-L64","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nFlowableOperator<Integer, Integer> op = child -> {\n    if (!enabled) return null; // -> NPE on subscribe\n    return new MapSubscriber<>(child, x -> x + 1);\n};\n// after\nFlowableOperator<Integer, Integer> op = child -> {\n    if (!enabled) return child;  // pass-through, never null\n    return new MapSubscriber<>(child, x -> x + 1);\n};","handlingStrategy":"validation","validationCode":"// Validate a custom FlowableOperator never returns null before use.\nFlowableOperator<R, T> op = /* your operator */;\nFlowableOperator<R, T> safeOp = child -> {\n    Subscriber<? super T> s = op.apply(child);\n    if (s == null) {\n        throw new NullPointerException(\"Operator \" + op + \" returned null Subscriber\");\n    }\n    return s;\n};\n// Better: unit-test every branch of apply() for non-null output before wiring it in.","typeGuard":"static <T, R> boolean operatorNeverReturnsNull(FlowableOperator<R, T> op) {\n    // Best-effort static check: inspect that no 'return null;' literal exists;\n    // runtime guarantee requires a test subscriber probe per branch.\n    return op != null; // a real guarantee needs branch-coverage tests, see validationCode\n}","tryCatchPattern":"try {\n    flowable.lift(op).subscribe(sub);\n} catch (NullPointerException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"returned a null Subscriber\")) {\n        // custom operator returned null; fix apply() to return a real Subscriber\n        log.error(\"Custom operator returned null Subscriber: \" + op, e);\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["rxjava","null-pointer","lift","custom-operator","reactive-streams","flowable"],"backgroundTag":null,"analyzedSha":"a8ab5356143f37a8f1dd6d76c79191bec5fa343b","analyzedAt":"2026-08-13T23:25:30.069Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}