tokio-rs/tokio · critical

cannot enter a task-local scope during or after destruction…

Error message

cannot enter a task-local scope during or after destruction of the underlying thread-local

What it means

Task-local values are stored in thread-local storage. `scope_inner` first probes the thread-local with `self.inner.try_with(...)` (task_local.rs:209); if the thread-local is mid-destruction or already destroyed, std returns `std::thread::AccessError`, converted to `ScopeInnerErr::AccessError` and panicked at line 473. Entering a task-local scope is only valid while the backing thread-local still exists.

Solutions

  1. Never access or enter task-locals from `Drop` impls that may execute during thread/TLS shutdown.
  2. Make sure all tasks and their resources are fully dropped before the `Runtime` is dropped and its worker threads exit.
  3. Capture any task-local state you need at shutdown into owned data before the runtime is dropped.
  4. If you must probe safely, use `try_with` / `try_with_value` (returning `AccessError`) instead of the panicking `with` / `scope` variants.

Example fix

// before — Drop touches a task-local during teardown
impl Drop for Guard {
    fn drop(&mut self) { VALUE.with(|v| { /* ... */ }); } // may panic at shutdown
}

// after — fail-soft probe that tolerates a destroyed TLS
impl Drop for Guard {
    fn drop(&mut self) {
        let _ = VALUE.try_with(|v| { /* ... */ });
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Use the fallible variant in any context that may run during teardown.
let _ = VALUE.try_with(|v| { /* ... */ }); // returns Err(AccessError) instead of panicking

Try / catch

std::panic::catch_unwind(|| {
    VALUE.sync_scope(x, || { /* ... */ })
}).ok();

Prevention

When it happens

Trigger: Calling `sync_scope`/`scope` from a `Drop` implementation that runs during thread-local teardown, or after the owning worker thread / runtime has already begun shutting down and its TLS slot is gone.

Common situations: Custom smart pointers or guards whose `Drop` touches a task-local while the runtime worker thread is exiting; resources whose destructors observe a task-local during process teardown; background threads spawned outside the runtime that outlive it.

Related errors


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/0566817f8fb3265d. Report an issue: GitHub.

Appendix: source

Thrown at tokio/src/task/task_local.rs:473

impl fmt::Display for AccessError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt("task-local value not set", f)
    }
}

impl Error for AccessError {}

enum ScopeInnerErr {
    BorrowError,
    AccessError,
}

impl ScopeInnerErr {
    #[track_caller]
    fn panic(&self) -> ! {
        match self {
            Self::BorrowError => panic!("cannot enter a task-local scope while the task-local storage is borrowed"),
            Self::AccessError => panic!("cannot enter a task-local scope during or after destruction of the underlying thread-local"),
        }
    }
}

impl From<std::cell::BorrowMutError> for ScopeInnerErr {
    fn from(_: std::cell::BorrowMutError) -> Self {
        Self::BorrowError
    }
}

impl From<std::thread::AccessError> for ScopeInnerErr {
    fn from(_: std::thread::AccessError) -> Self {
        Self::AccessError
    }
}

View on GitHub (pinned to 625954f365)