linebender/druid · error

The main thread status has already been claimed by thread

Error message

The main thread status has already been claimed by thread {k}

What it means

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.

Solutions

  1. Ensure only one thread calls `claim_main_thread`/runs the event loop, typically the process's actual main thread
  2. Verify the thread that previously claimed the status called `release_main_thread` before exiting (check for panics/early returns that skipped it)
  3. If embedding, restructure so platform UI work is marshalled to the single owning thread instead of claiming from multiple threads
  4. Add tracing on claim/release to find the thread `k` that leaked the claim

Example fix

// before
std::thread::spawn(|| {
    druid_shell::util::claim_main_thread();
    run_event_loop();
});
// after
// run the event loop on the process main thread only
fn main() {
    druid_shell::util::claim_main_thread();
    run_event_loop();
    druid_shell::util::release_main_thread();
}
Defensive patterns

Strategy: validation

Validate before calling

use std::sync::atomic::{AtomicUsize, Ordering};
// Before claiming, check the current holder:
fn can_claim(main_thread_id: &AtomicUsize) -> bool {
    main_thread_id.load(Ordering::Acquire) == 0
}

Type guard

fn is_unclaimed(main_thread_id: &AtomicUsize) -> bool {
    main_thread_id.load(Ordering::Acquire) == 0
}

Try / catch

// panics cannot be caught idiomatically in Rust; avoid via validation
if is_unclaimed(&MAIN_THREAD_ID) {
    claim_main_thread();
} else {
    tracing::warn!("main thread already claimed; skipping claim");
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10). Data as JSON: /api/errors/b13207fb32e16e6b. Report an issue: GitHub.

Appendix: source

Thrown at druid-shell/src/util.rs:47

    }
}

/// 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.")
        }
        Err(k) => panic!("The main thread status has already been claimed by thread {k}"),
    }
}

/// Removes the main thread status of the current thread.
///
/// # Panics
///
/// Panics if the main thread status is owned by another thread.
pub(crate) fn release_main_thread() {
    let thread_id = current_thread_id();
    let old_thread_id =
        MAIN_THREAD_ID.compare_exchange(thread_id, 0, Ordering::AcqRel, Ordering::Acquire);
    match old_thread_id {
        Ok(n) if n == thread_id => (),
        Ok(_) => unreachable!(), // not possible per the docs
        Err(0) => tracing::warn!("The main thread status was already vacant."),
        Err(k) => panic!("The main thread status has already been claimed by thread {k}"),
    }

View on GitHub (pinned to 0f8b1195e4)