{"record":{"id":"610c053abff476e1","repo":"ReactiveX/RxAndroid","slug":"looper-null","errorCode":null,"errorMessage":"looper == null","messagePattern":"looper == null","errorType":"exception","errorClass":"NullPointerException","httpStatus":null,"severity":"error","filePath":"rxandroid/src/main/java/io/reactivex/rxjava3/android/schedulers/AndroidSchedulers.java","lineNumber":64,"sourceCode":"     * A {@link Scheduler} which executes actions on {@code looper}.\n     * <p>\n     * The returned scheduler will post asynchronous messages to the looper by default.\n     *\n     * @see #from(Looper, boolean)\n     */\n    public static Scheduler from(Looper looper) {\n        return from(looper, true);\n    }\n\n    /**\n     * A {@link Scheduler} which executes actions on {@code looper}.\n     *\n     * @param async if true, the scheduler will use async messaging on API >= 16 to avoid VSYNC\n     *              locking. On API < 16 this value is ignored.\n     * @see Message#setAsynchronous(boolean)\n     */\n    public static Scheduler from(Looper looper, boolean async) {\n        if (looper == null) throw new NullPointerException(\"looper == null\");\n        return internalFrom(looper, async);\n    }\n\n    @SuppressLint(\"NewApi\") // Checking for an @hide API.\n    private static Scheduler internalFrom(Looper looper, boolean async) {\n        // Below code exists in androidx-core as well, but is left here rather than include an\n        // entire extra dependency.\n        // https://developer.android.com/reference/kotlin/androidx/core/os/MessageCompat?hl=en#setAsynchronous(android.os.Message,%20kotlin.Boolean)\n        if (Build.VERSION.SDK_INT < 16) {\n            async = false;\n        } else if (async && Build.VERSION.SDK_INT < 22) {\n            // Confirm that the method is available on this API level despite being @hide.\n            Message message = Message.obtain();\n            try {\n                message.setAsynchronous(true);\n            } catch (NoSuchMethodError e) {\n                async = false;\n            }","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/ReactiveX/RxAndroid/blob/afaea280461456b30e6ea608428517476ddb8b39/rxandroid/src/main/java/io/reactivex/rxjava3/android/schedulers/AndroidSchedulers.java#L46-L82","documentation":"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.","triggerScenarios":"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).","commonSituations":"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().","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."],"exampleFix":"// before\nnew Thread(() -> {\n    Scheduler s = AndroidSchedulers.from(Looper.myLooper()); // myLooper() == null -> NPE\n}).start();\n\n// after\nHandlerThread ht = new HandlerThread(\"work\");\nht.start();\nScheduler s = AndroidSchedulers.from(ht.getLooper());","handlingStrategy":"validation","validationCode":"Looper looper = Looper.myLooper();\nif (looper == null) {\n    // this thread has no looper: use a dedicated HandlerThread or the main looper\n    looper = Looper.getMainLooper();\n}\nScheduler s = AndroidSchedulers.from(looper);","typeGuard":"public static boolean hasUsableLooper(Looper looper) {\n    return looper != null;\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["rxandroid","rxjava","android","null-pointer","looper","handler-thread"],"backgroundTag":null,"analyzedSha":"afaea280461456b30e6ea608428517476ddb8b39","analyzedAt":"2026-08-14T12:58:42.270Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}