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

  1. 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.
  2. If a background thread must update UI, use ExtCtx/scheduler (druid's widget-level scheduling) instead of touching shell APIs directly.
  3. 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.
  4. 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

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


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)