{"record":{"id":"4660e8e05e779660","repo":"ReactiveX/RxAndroid","slug":"expected-to-be-called-on-the-main-thread-but-was","errorCode":null,"errorMessage":"Expected to be called on the main thread but was {threadName}","messagePattern":"Expected to be called on the main thread but was (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"rxandroid/src/main/java/io/reactivex/rxjava3/android/MainThreadDisposable.java","lineNumber":57,"sourceCode":" *     &#064;Override protected void onDispose() {\n *       // TODO undo behavior\n *     }\n *   });\n * }\n * </code></pre>\n */\npublic abstract class MainThreadDisposable implements Disposable {\n    /**\n     * Verify that the calling thread is the Android main thread.\n     * <p>\n     * Calls to this method are usually preconditions for subscription behavior which instances of\n     * this class later undo. See the class documentation for an example.\n     *\n     * @throws IllegalStateException when called from any other thread.\n     */\n    public static void verifyMainThread() {\n        if (Looper.myLooper() != Looper.getMainLooper()) {\n            throw new IllegalStateException(\n                \"Expected to be called on the main thread but was \" + Thread.currentThread().getName());\n        }\n    }\n\n    private final AtomicBoolean unsubscribed = new AtomicBoolean();\n\n    @Override\n    public final boolean isDisposed() {\n        return unsubscribed.get();\n    }\n\n    @Override\n    public final void dispose() {\n        if (unsubscribed.compareAndSet(false, true)) {\n            if (Looper.myLooper() == Looper.getMainLooper()) {\n                onDispose();\n            } else {\n                AndroidSchedulers.mainThread().scheduleDirect(this::onDispose);","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/ReactiveX/RxAndroid/blob/afaea280461456b30e6ea608428517476ddb8b39/rxandroid/src/main/java/io/reactivex/rxjava3/android/MainThreadDisposable.java#L39-L75","documentation":"RxAndroid throws this IllegalStateException from MainThreadDisposable.verifyMainThread() when an observer or disposable that requires the Android main thread (e.g. RxBinding view observers, MainThreadDisposable subclasses) is subscribed, disposed, or otherwise used from a non-main thread. The check compares Looper.myLooper() with Looper.getMainLooper() and includes the offending thread's name in the message. It exists because view/UI operations and listener registration/removal are only safe on the main thread, and the library enforces that as a precondition rather than risking undefined UI behavior.","triggerScenarios":"Subscribing an observer that extends MainThreadDisposable (such as RxBinding's view observers) from a background thread; calling dispose() on such an observer off the main thread; emitting into such an observer from an IO/computation scheduler without observeOn(AndroidSchedulers.mainThread()); unit tests that subscribe on a plain JUnit thread with no Android looper at all.","commonSituations":"Piping a network or database stream straight into a view observer without observeOn(mainThread()); calling dispose() inside a Schedulers.io() lambda or a Retrofit callback thread; refactoring a subscribe call out of onCreate into a background worker; running Android unit tests (non-Robolectric) where Looper.getMainLooper() is null or different, causing confusing failures; mixing Kotlin coroutines that resume on Dispatchers.IO and then touching RxBinding subscriptions.","solutions":["Add .observeOn(AndroidSchedulers.mainThread()) as the last step before subscribing a UI-bound observer.","Move the subscribe() or dispose() call onto the main thread, e.g. new Handler(Looper.getMainLooper()).post(...) or Activity#runOnUiThread(...).","If disposing from a background thread, dispose the upstream Disposable directly (the one returned by subscribe()) instead of the MainThreadDisposable instance, or post the dispose to the main thread.","In unit tests, run with Robolectric (@RunWith(RobolectricTestRunner.class)) so a real main looper exists, or use RxAndroidPlugins.setMainThreadSchedulerHandler to swap in Schedulers.trampoline() and avoid looper-dependent observers.","If you implemented MainThreadDisposable yourself, re-read its class docs: verifyMainThread() is meant to be called in onSubscribe and undone in onDispose, both of which must then happen on the main thread."],"exampleFix":"// before\napiService.loadUser()\n    .subscribeOn(Schedulers.io())\n    .subscribe(userViewObserver); // MainThreadDisposable-based observer -> ISE\n\n// after\napiService.loadUser()\n    .subscribeOn(Schedulers.io())\n    .observeOn(AndroidSchedulers.mainThread())\n    .subscribe(userViewObserver);","handlingStrategy":"validation","validationCode":"boolean isOnMainThread = Looper.myLooper() == Looper.getMainLooper();\nif (!isOnMainThread) {\n    // route the work to the main thread instead of subscribing/disposing here\n    new Handler(Looper.getMainLooper()).post(() -> doSubscribeOrDispose());\n}","typeGuard":null,"tryCatchPattern":"try {\n    MainThreadDisposable.verifyMainThread(); // or the risky subscribe/dispose\n} catch (IllegalStateException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"main thread\")) {\n        new Handler(Looper.getMainLooper()).post(() -> doSubscribeOrDispose());\n    } else {\n        throw e;\n    }\n}","preventionTips":["End every UI-bound chain with .observeOn(AndroidSchedulers.mainThread()) before subscribe().","Only call dispose() on the Disposable returned by subscribe(), or post disposal to the main thread.","Run Android unit tests that touch UI observers with Robolectric, and swap in Schedulers.trampoline() via RxAndroidPlugins for pure logic tests.","Treat this ISE as a design signal: do not touch view observers from IO/computation callbacks."],"tags":["rxandroid","rxjava","android","threading","main-thread","looper"],"backgroundTag":null,"analyzedSha":"afaea280461456b30e6ea608428517476ddb8b39","analyzedAt":"2026-08-14T12:58:42.270Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}