{"record":{"id":"e60c0ca19c38879f","repo":"greenrobot/EventBus","slug":"could-not-send-handler-message","errorCode":null,"errorMessage":"Could not send handler message","messagePattern":"Could not send handler message","errorType":"exception","errorClass":"EventBusException","httpStatus":null,"severity":"error","filePath":"eventbus-android/src/main/java/org/greenrobot/eventbus/HandlerPoster.java","lineNumber":44,"sourceCode":"    private final int maxMillisInsideHandleMessage;\n    private final EventBus eventBus;\n    private boolean handlerActive;\n\n    public HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {\n        super(looper);\n        this.eventBus = eventBus;\n        this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;\n        queue = new PendingPostQueue();\n    }\n\n    public void enqueue(Subscription subscription, Object event) {\n        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);\n        synchronized (this) {\n            queue.enqueue(pendingPost);\n            if (!handlerActive) {\n                handlerActive = true;\n                if (!sendMessage(obtainMessage())) {\n                    throw new EventBusException(\"Could not send handler message\");\n                }\n            }\n        }\n    }\n\n    @Override\n    public void handleMessage(Message msg) {\n        boolean rescheduled = false;\n        try {\n            long started = SystemClock.uptimeMillis();\n            while (true) {\n                PendingPost pendingPost = queue.poll();\n                if (pendingPost == null) {\n                    synchronized (this) {\n                        // Check again, this time in synchronized\n                        pendingPost = queue.poll();\n                        if (pendingPost == null) {\n                            handlerActive = false;","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/greenrobot/EventBus/blob/0194926b3bcf70cc0d7bfd3c5da16708dd5ab876/eventbus-android/src/main/java/org/greenrobot/eventbus/HandlerPoster.java#L26-L62","documentation":"Thrown from HandlerPoster.enqueue() (the Android main-thread poster used for ThreadMode.MAIN delivery) when sendMessage(obtainMessage()) returns false. Handler.sendMessage returns false only when the message was not accepted — practically, when the Handler's Looper has quit (isQuitting/quitted) so no message can be enqueued. HandlerPoster rides the main Looper; enqueue() runs synchronized on the poster and flips handlerActive=true before sending, so a failed send leaves the poster marked active with a dead queue — hence the hard crash rather than silent event loss.","triggerScenarios":"Posting an event with a MAIN-thread subscriber while the target Looper is shutting down: posting from a background thread during process teardown, or a HandlerPoster constructed with a Looper from a dying thread (e.g. a test or instrumentation thread whose Looper.quit() was called, or EventBus instance outliving an Android component's lifecycle in unit tests using LooperMode).","commonSituations":"Robolectric/JVM tests that pause or quit the main looper while async posts are in flight; posting from a thread during Application shutdown; custom EventBus instances created against a worker Looper that then quits; race where a post lands after the process has begun terminating.","solutions":["Ensure EventBus instances and posts do not outlive the Looper they deliver on: stop/cancel background producers before quitting the Looper","In Robolectric tests, use Shadows.shadowOf(Looper.getMainLooper()).idle() to drain pending MAIN posts instead of quitting the looper","Unregister subscribers and stop posting during component teardown (onDestroy of the posting producer)","If using a custom Looper-scoped EventBus, keep that Looper alive for the bus's lifetime"],"exampleFix":"// before (Robolectric test)\nbackgroundExecutor.submit(() -> EventBus.getDefault().post(new UiEvent()));\nshadowOf(Looper.getMainLooper()).quit(); // MAIN poster cannot send -> throws\n\n// after\nbackgroundExecutor.submit(() -> EventBus.getDefault().post(new UiEvent()));\nshadowOf(Looper.getMainLooper()).idle(); // drain queue on a live looper","handlingStrategy":"validation","validationCode":"// Before posting a MAIN-delivered event from a background thread at teardown time\nif (isShuttingDown()) { // your app-level shutdown flag set in onDestroy/onTerminate\n    return;\n}\nEventBus.getDefault().post(new UiEvent());","typeGuard":null,"tryCatchPattern":"try {\n    EventBus.getDefault().post(event); // may reach HandlerPoster.enqueue on a dying looper\n} catch (EventBusException e) {\n    if (\"Could not send handler message\".equals(e.getMessage())) {\n        // looper quit during teardown: drop the event, app is going down anyway\n        Log.w(TAG, \"Main looper unavailable, discarding \" + event.getClass().getSimpleName());\n        return;\n    }\n    throw e;\n}","preventionTips":["Stop background producers (threads, executors) before quitting loopers/process teardown","In Robolectric, drain with shadowOf(Looper.getMainLooper()).idle() instead of quit()","Never construct EventBus/HandlerPoster against a Looper that has a shorter lifetime than the bus"],"tags":["eventbus","android","looper","handler","main-thread","teardown"],"backgroundTag":null,"analyzedSha":"0194926b3bcf70cc0d7bfd3c5da16708dd5ab876","analyzedAt":"2026-08-14T10:41:08.786Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}