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
- Make the Callable return a real scheduler: () -> Schedulers.trampoline() for tests, or () -> AndroidSchedulers.from(Looper.getMainLooper()) for runtime.
- Fix the mock: when(callable.call()).thenReturn(testScheduler) — never leave call() unstubbed.
- Add Objects.requireNonNull inside your Callable so construction fails loudly at the true source: () -> Objects.requireNonNull(buildScheduler(), "buildScheduler").
- 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
- Never let a scheduler factory return null; throw a descriptive exception from inside the Callable instead.
- Stub every mocked Callable/Scheduler: when(callable.call()).thenReturn(scheduler).
- In Kotlin, declare factory lambdas as () -> Scheduler (non-null return type) so 'return null' does not compile.
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
- scheduler == null
- run == null
- looper == null
- Expected to be called on the main thread but was {threadName
AI-assisted analysis of ReactiveX/RxAndroid@afaea28046 (2026-08-14).
Data as JSON: /api/errors/2724741f623dadc3.
Report an issue: GitHub.