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

  1. 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());
  2. Guard the call: if (looper != null) { ... } else { fall back to Schedulers.single()/from(Looper.getMainLooper()) }.
  3. In unit tests, use Robolectric or inject Schedulers.trampoline() instead of touching real Loopers.
  4. 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

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


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