linebender/druid · critical
Main thread assertion failed
Error message
Main thread assertion failed {thread_id} != {main_thread_id} What it means
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.
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.
Example fix
// before: building/running the app on a spawned thread
std::thread::spawn(|| {
let mut win = WindowDesc::new(ui_builder);
AppLauncher::with_window(win).launch().unwrap();
});
// after: launch on the main thread, offload work to a worker
std::thread::spawn(move || {
let data = load_data_blocking();
event_sink.submit_command(RECV_DATA, data, Target::Auto);
});
AppLauncher::with_window(WindowDesc::new(ui_builder))
.launch()
.unwrap(); Defensive patterns
Strategy: validation
Validate before calling
// guard your own entry points std::thread::current().name().map_or(false, |n| n == "main"); // or verify before shell calls: // MAIN_THREAD check is internal; keep all druid-shell calls on the launching thread
Try / catch
let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
window.text(); // any main-thread-only shell call
}));
if ok.is_err() { eprintln!("druid-shell API called off the main thread"); } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- unexpected wayland event
- The main thread status has already been claimed by thread
- keyboard event processing failed
- Unwrap named called on unnamed FieldIdent
- Application is already running
AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10).
Data as JSON: /api/errors/5dcf5abc33fc0e6c.
Report an issue: GitHub.
Appendix: source
Thrown at druid-shell/src/util.rs:28
static MAIN_THREAD_ID: AtomicU64 = AtomicU64::new(0);
#[inline]
fn current_thread_id() -> u64 {
// TODO: Use .as_u64() instead of mem::transmute
// when .as_u64() or something similar gets stabilized.
unsafe { mem::transmute(thread::current().id()) }
}
/// Assert that the current thread is the registered main thread or main thread is not claimed.
///
/// # Panics
///
/// Panics when called from a non-main thread and main thread is claimed.
pub(crate) fn assert_main_thread_or_main_unclaimed() {
let thread_id = current_thread_id();
let main_thread_id = MAIN_THREAD_ID.load(Ordering::Acquire);
if thread_id != main_thread_id && main_thread_id != 0 {
panic!("Main thread assertion failed {thread_id} != {main_thread_id}");
}
}
/// Register the current thread as the main thread.
///
/// # Panics
///
/// Panics if the main thread has already been claimed by another thread.
pub(crate) fn claim_main_thread() {
let thread_id = current_thread_id();
let old_thread_id =
MAIN_THREAD_ID.compare_exchange(0, thread_id, Ordering::AcqRel, Ordering::Acquire);
match old_thread_id {
Ok(0) => (),
Ok(_) => unreachable!(), // not possible per the docs
Err(0) => {
tracing::warn!("The main thread status was already claimed by the current thread.")
}View on GitHub (pinned to 0f8b1195e4)