ReactiveX/RxAndroid · error · NullPointerException

Scheduler Callable returned null

Error message

Scheduler Callable returned null

What it means

callRequireNonNull is the default path of RxAndroidPlugins.initMainThreadScheduler: it invokes the Scheduler Callable and throws this NullPointerException when call() returns null. The message 'Scheduler Callable returned null' distinguishes it from a null Callable argument — the Callable ran but produced nothing. This is RxAndroid's equivalent of RxJava's plugin contract: scheduler factories must never yield null.

Source

Thrown at rxandroid/src/main/java/io/reactivex/rxjava3/android/plugins/RxAndroidPlugins.java:87

     * @return the hook function, may be null
     */
    public static Function<Scheduler, Scheduler> getOnMainThreadSchedulerHandler() {
        return onMainThreadHandler;
    }

    /**
     * Removes all handlers and resets the default behavior.
     */
    public static void reset() {
        setInitMainThreadSchedulerHandler(null);
        setMainThreadSchedulerHandler(null);
    }

    static Scheduler callRequireNonNull(Callable<Scheduler> s) {
        try {
            Scheduler scheduler = s.call();
            if (scheduler == null) {
                throw new NullPointerException("Scheduler Callable returned null");
            }
            return scheduler;
        } catch (Throwable ex) {
            throw Exceptions.propagate(ex);
        }
    }

    static Scheduler applyRequireNonNull(Function<Callable<Scheduler>, Scheduler> f, Callable<Scheduler> s) {
        Scheduler scheduler = apply(f,s);
        if (scheduler == null) {
            throw new NullPointerException("Scheduler Callable returned null");
        }
        return scheduler;
    }

    static <T, R> R apply(Function<T, R> f, T t) {
        try {
            return f.apply(t);

View on GitHub (pinned to afaea28046)

Solutions

  1. Make the Callable return a real scheduler: () -> Schedulers.trampoline() for tests, or () -> AndroidSchedulers.from(Looper.getMainLooper()) for runtime.
  2. Fix the mock: when(callable.call()).thenReturn(testScheduler) — never leave call() unstubbed.
  3. Add Objects.requireNonNull inside your Callable so construction fails loudly at the true source: () -> Objects.requireNonNull(buildScheduler(), "buildScheduler").
  4. If a Throwable from call() is being rethrown, note callRequireNonNull wraps it via Exceptions.propagate — inspect the cause chain in the stack trace.

Example fix

// before
RxAndroidPlugins.setInitMainThreadSchedulerHandler(c -> null); // handler path
// or a Callable returning null:
Callable<Scheduler> c = () -> maybeScheduler; // null field -> NPE "Scheduler Callable returned null"

// after
RxAndroidPlugins.setInitMainThreadSchedulerHandler(c -> Schedulers.trampoline());
Defensive patterns

Strategy: validation

Validate before calling

Callable<Scheduler> safe = () -> {
    Scheduler s = buildScheduler();
    if (s == null) throw new IllegalStateException("buildScheduler() returned null");
    return s;
};
RxAndroidPlugins.initMainThreadScheduler(safe);

Try / catch

try {
    return RxAndroidPlugins.initMainThreadScheduler(callable);
} catch (NullPointerException e) {
    if ("Scheduler Callable returned null".equals(e.getMessage())) {
        return Schedulers.trampoline(); // safe default rather than crashing bootstrap
    }
    throw e;
}

Prevention

When it happens

Trigger: A custom Callable passed to (or reached via) initMainThreadScheduler whose call() returns null, e.g. because a lazily-created HandlerScheduler was never assigned; a mocked Callable stubbed with thenReturn(null); a Kotlin lambda like { null } inferred as Callable<Scheduler?>; AndroidSchedulers.from(...) invoked with a null Looper inside the Callable (the NPE propagates through Exceptions.propagate with a different message).

Common situations: Test setup that mocks the scheduler callable but forgets to stub call(); a test double whose default answer returns null (Mockito RETURNS_DEFAULTS); initialization code that reads a scheduler from a nullable map or DI container that has not been populated yet; refactoring where the Callable body loses its return statement's value.

Related errors


AI-assisted analysis of ReactiveX/RxAndroid@afaea28046 (2026-08-14). Data as JSON: /api/errors/2724741f623dadc3. Report an issue: GitHub.