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
- Never access or enter task-locals from `Drop` impls that may execute during thread/TLS shutdown.
- Make sure all tasks and their resources are fully dropped before the `Runtime` is dropped and its worker threads exit.
- Capture any task-local state you need at shutdown into owned data before the runtime is dropped.
- 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
- Avoid touching task-locals in `Drop` impls that may execute during thread-local teardown.
- Drop/cancel all tasks before dropping the `Runtime`.
- Prefer `try_with`/`try_with_value` (which return `Result`, not panic) in any destructor-adjacent code path.
- Test shutdown paths explicitly (e.g. `drop(runtime)` with live tasks) under a debugger.
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
- cannot enter a task-local scope while the task-local…
- stream did not contain valid UTF-8
- `name` set multiple times.
- `flavor` set multiple times.
- The single threaded runtime flavor is called…
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)