{"record":{"id":"5dcf5abc33fc0e6c","repo":"linebender/druid","slug":"main-thread-assertion-failed-thread-id-main","errorCode":null,"errorMessage":"Main thread assertion failed {thread_id} != {main_thread_id}","messagePattern":"Main thread assertion failed (.+?) != (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"druid-shell/src/util.rs","lineNumber":28,"sourceCode":"static MAIN_THREAD_ID: AtomicU64 = AtomicU64::new(0);\n\n#[inline]\nfn current_thread_id() -> u64 {\n    // TODO: Use .as_u64() instead of mem::transmute\n    // when .as_u64() or something similar gets stabilized.\n    unsafe { mem::transmute(thread::current().id()) }\n}\n\n/// Assert that the current thread is the registered main thread or main thread is not claimed.\n///\n/// # Panics\n///\n/// Panics when called from a non-main thread and main thread is claimed.\npub(crate) fn assert_main_thread_or_main_unclaimed() {\n    let thread_id = current_thread_id();\n    let main_thread_id = MAIN_THREAD_ID.load(Ordering::Acquire);\n    if thread_id != main_thread_id && main_thread_id != 0 {\n        panic!(\"Main thread assertion failed {thread_id} != {main_thread_id}\");\n    }\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        }","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/linebender/druid/blob/0f8b1195e4e073f9597f2865299c3d18f8e4005f/druid-shell/src/util.rs#L10-L46","documentation":"druid-shell requires that UI/shell APIs run on the thread that first claimed the main thread role. assert_main_thread_or_main_unclaimed reads the stored MAIN_THREAD_ID and panics if called from any thread other than the recorded main thread while a main thread is already claimed. This guards win32/macOS event loops and other main-thread-only platform APIs.","triggerScenarios":"Calling druid-shell APIs (window creation, Application::run, etc.) from a spawned thread (std::thread::spawn, rayon pool, async runtime worker) after another thread — typically the one that ran Application::init — already registered itself as the main thread.","commonSituations":"Spawning the druid app in a background thread while something else (or an earlier init) claimed main-thread status; calling window/app APIs from tokio/async worker threads; embedding druid-shell inside a framework that owns the real main thread while druid runs on a worker.","solutions":["Run Application::init and all window/druid-shell calls on the process's main thread; spawn worker threads for background work and communicate results back via channels or the shell's idle/timer callbacks.","If a background thread must update UI, use ExtCtx/scheduler (druid's widget-level scheduling) instead of touching shell APIs directly.","Audit early startup code so the first thread to call init is genuinely the main thread; avoid initializing the app inside a thread pool task.","If you control the code, thread_id != main_thread_id cases can be surfaced earlier by adding the assertion at your own API boundaries to fail fast in development."],"exampleFix":"// before: building/running the app on a spawned thread\nstd::thread::spawn(|| {\n    let mut win = WindowDesc::new(ui_builder);\n    AppLauncher::with_window(win).launch().unwrap();\n});\n// after: launch on the main thread, offload work to a worker\nstd::thread::spawn(move || {\n    let data = load_data_blocking();\n    event_sink.submit_command(RECV_DATA, data, Target::Auto);\n});\nAppLauncher::with_window(WindowDesc::new(ui_builder))\n    .launch()\n    .unwrap();","handlingStrategy":"validation","validationCode":"// guard your own entry points\nstd::thread::current().name().map_or(false, |n| n == \"main\");\n// or verify before shell calls:\n// MAIN_THREAD check is internal; keep all druid-shell calls on the launching thread","typeGuard":null,"tryCatchPattern":"let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    window.text(); // any main-thread-only shell call\n}));\nif ok.is_err() { eprintln!(\"druid-shell API called off the main thread\"); }","preventionTips":["Always launch the app from the process's main thread (fn main).","Never call druid-shell APIs directly from spawned threads, tokio workers, or rayon pools.","Send background results back via commands/event sinks rather than touching windows cross-thread.","Keep initialization order deterministic so the main thread is the first to claim it."],"tags":["threading","main-thread","panic","event-loop"],"backgroundTag":"wrong-thread-assertion","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"}