{"record":{"id":"b13207fb32e16e6b","repo":"linebender/druid","slug":"the-main-thread-status-has-already-been-claimed-by","errorCode":null,"errorMessage":"The main thread status has already been claimed by thread {k}","messagePattern":"The main thread status has already been claimed by thread (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"druid-shell/src/util.rs","lineNumber":47,"sourceCode":"    }\n}\n\n/// Register the current thread as the main thread.\n///\n/// # Panics\n///\n/// Panics if the main thread has already been claimed by another thread.\npub(crate) fn claim_main_thread() {\n    let thread_id = current_thread_id();\n    let old_thread_id =\n        MAIN_THREAD_ID.compare_exchange(0, thread_id, Ordering::AcqRel, Ordering::Acquire);\n    match old_thread_id {\n        Ok(0) => (),\n        Ok(_) => unreachable!(), // not possible per the docs\n        Err(0) => {\n            tracing::warn!(\"The main thread status was already claimed by the current thread.\")\n        }\n        Err(k) => panic!(\"The main thread status has already been claimed by thread {k}\"),\n    }\n}\n\n/// Removes the main thread status of the current thread.\n///\n/// # Panics\n///\n/// Panics if the main thread status is owned by another thread.\npub(crate) fn release_main_thread() {\n    let thread_id = current_thread_id();\n    let old_thread_id =\n        MAIN_THREAD_ID.compare_exchange(thread_id, 0, Ordering::AcqRel, Ordering::Acquire);\n    match old_thread_id {\n        Ok(n) if n == thread_id => (),\n        Ok(_) => unreachable!(), // not possible per the docs\n        Err(0) => tracing::warn!(\"The main thread status was already vacant.\"),\n        Err(k) => panic!(\"The main thread status has already been claimed by thread {k}\"),\n    }","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/linebender/druid/blob/0f8b1195e4e073f9597f2865299c3d18f8e4005f/druid-shell/src/util.rs#L29-L65","documentation":"This panic comes from `claim_main_thread` in druid-shell's util.rs, which manages an atomic MAIN_THREAD_ID flag ensuring main-thread-only work (e.g. platform event loop calls) happens on exactly one thread. It fires when the flag is already held by a different thread `k`, meaning another thread claimed main-thread status and never released it. The library panics because proceeding would violate the platform's single-main-thread guarantees.","triggerScenarios":"Calling `claim_main_thread` from a thread while `MAIN_THREAD_ID` still holds a non-zero ID from another thread; a previously-claimed thread crashed or exited without calling `release_main_thread`; claiming from two threads concurrently in tests spawning multiple UI threads.","commonSituations":"Spawning the druid shell/event loop on a non-main thread while another worker also claims it; tests that spin up multiple app instances on different threads; a leak from a prior run in a long-lived process (embedders, FFI hosts) that never released the claim.","solutions":["Ensure only one thread calls `claim_main_thread`/runs the event loop, typically the process's actual main thread","Verify the thread that previously claimed the status called `release_main_thread` before exiting (check for panics/early returns that skipped it)","If embedding, restructure so platform UI work is marshalled to the single owning thread instead of claiming from multiple threads","Add tracing on claim/release to find the thread `k` that leaked the claim"],"exampleFix":"// before\nstd::thread::spawn(|| {\n    druid_shell::util::claim_main_thread();\n    run_event_loop();\n});\n// after\n// run the event loop on the process main thread only\nfn main() {\n    druid_shell::util::claim_main_thread();\n    run_event_loop();\n    druid_shell::util::release_main_thread();\n}","handlingStrategy":"validation","validationCode":"use std::sync::atomic::{AtomicUsize, Ordering};\n// Before claiming, check the current holder:\nfn can_claim(main_thread_id: &AtomicUsize) -> bool {\n    main_thread_id.load(Ordering::Acquire) == 0\n}\n","typeGuard":"fn is_unclaimed(main_thread_id: &AtomicUsize) -> bool {\n    main_thread_id.load(Ordering::Acquire) == 0\n}\n","tryCatchPattern":"// panics cannot be caught idiomatically in Rust; avoid via validation\nif is_unclaimed(&MAIN_THREAD_ID) {\n    claim_main_thread();\n} else {\n    tracing::warn!(\"main thread already claimed; skipping claim\");\n}\n","preventionTips":["Run the event loop only on the process main thread","Always pair claim with release on the same thread (RAII guard)","In tests, use a mutex to serialize threads that claim main-thread status","Check for early returns/panics in the claiming thread that skip release"],"tags":["rust","threading","druid-shell","main-thread"],"backgroundTag":"internal-invariant-violation","analyzedSha":"0f8b1195e4e073f9597f2865299c3d18f8e4005f","analyzedAt":"2026-09-10T14:27:34.582Z","contentChangedAt":"2026-09-10T14:27:34.582Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}