ReactiveX/RxAndroid · error · NullPointerException
looper == null
Error message
looper == null
What it means
AndroidSchedulers.from(Looper, boolean) (and the one-arg overload delegating to it) throws this NullPointerException when the looper argument is null. The returned HandlerScheduler posts work onto that looper's Handler, so a null looper has no valid fallback. The most common real cause is not an explicit null literal but Looper.myLooper() returning null on a thread that has no looper prepared.
Source
Thrown at rxandroid/src/main/java/io/reactivex/rxjava3/android/schedulers/AndroidSchedulers.java:64
* A {@link Scheduler} which executes actions on {@code looper}.
* <p>
* The returned scheduler will post asynchronous messages to the looper by default.
*
* @see #from(Looper, boolean)
*/
public static Scheduler from(Looper looper) {
return from(looper, true);
}
/**
* A {@link Scheduler} which executes actions on {@code looper}.
*
* @param async if true, the scheduler will use async messaging on API >= 16 to avoid VSYNC
* locking. On API < 16 this value is ignored.
* @see Message#setAsynchronous(boolean)
*/
public static Scheduler from(Looper looper, boolean async) {
if (looper == null) throw new NullPointerException("looper == null");
return internalFrom(looper, async);
}
@SuppressLint("NewApi") // Checking for an @hide API.
private static Scheduler internalFrom(Looper looper, boolean async) {
// Below code exists in androidx-core as well, but is left here rather than include an
// entire extra dependency.
// https://developer.android.com/reference/kotlin/androidx/core/os/MessageCompat?hl=en#setAsynchronous(android.os.Message,%20kotlin.Boolean)
if (Build.VERSION.SDK_INT < 16) {
async = false;
} else if (async && Build.VERSION.SDK_INT < 22) {
// Confirm that the method is available on this API level despite being @hide.
Message message = Message.obtain();
try {
message.setAsynchronous(true);
} catch (NoSuchMethodError e) {
async = false;
}View on GitHub (pinned to afaea28046)
Solutions
- For a background handler thread: create and start a HandlerThread and use its looper — HandlerThread ht = new HandlerThread("worker"); ht.start(); AndroidSchedulers.from(ht.getLooper());
- Guard the call: if (looper != null) { ... } else { fall back to Schedulers.single()/from(Looper.getMainLooper()) }.
- In unit tests, use Robolectric or inject Schedulers.trampoline() instead of touching real Loopers.
- In Kotlin, make the parameter non-null (Looper not Looper?) so the compiler rejects nullable values at the call site.
Example fix
// before
new Thread(() -> {
Scheduler s = AndroidSchedulers.from(Looper.myLooper()); // myLooper() == null -> NPE
}).start();
// after
HandlerThread ht = new HandlerThread("work");
ht.start();
Scheduler s = AndroidSchedulers.from(ht.getLooper()); Defensive patterns
Strategy: validation
Validate before calling
Looper looper = Looper.myLooper();
if (looper == null) {
// this thread has no looper: use a dedicated HandlerThread or the main looper
looper = Looper.getMainLooper();
}
Scheduler s = AndroidSchedulers.from(looper); Type guard
public static boolean hasUsableLooper(Looper looper) {
return looper != null;
} Prevention
- For background scheduling, always start a HandlerThread and use handlerThread.getLooper(), never Looper.myLooper() from arbitrary threads.
- Null-check (or Kotlin type as non-null) any looper obtained from external state before calling from().
- In JVM unit tests, use Robolectric or avoid AndroidSchedulers.from entirely; inject Schedulers.trampoline() instead.
When it happens
Trigger: Calling AndroidSchedulers.from(Looper.myLooper()) on a plain background/IO thread where no looper exists (myLooper() returns null); unit tests executing on the JUnit main thread without Robolectric; passing a nullable Looper variable from Kotlin; obtaining the looper from HandlerThread.getLooper() before the thread has started (getLooper() blocks but can be null if the thread died).
Common situations: Trying to create a scheduler bound to a HandlerThread and accidentally using Looper.myLooper() instead of handlerThread.getLooper(); JVM unit tests without Robolectric where android.os.Looper is a stub returning null; Kotlin code where a Looper? flows into from(); devices/scenarios where a background thread was never prepared with Looper.prepare().
Related errors
- Expected to be called on the main thread but was {threadName
- scheduler == null
- Scheduler Callable returned null
- run == null
AI-assisted analysis of ReactiveX/RxAndroid@afaea28046 (2026-08-14).
Data as JSON: /api/errors/610c053abff476e1.
Report an issue: GitHub.