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
- 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
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
- 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
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
- Main thread assertion failed
- acquire_input_lock was called on a WinHandler that did not…
- release_input_lock was called on a WinHandler that did not…
- No path received for filename
- More than one path received for single selection
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)