{"id":"0566817f8fb3265d","repo":"tokio-rs/tokio","slug":"cannot-enter-a-task-local-scope-during-or-after-de","errorCode":null,"errorMessage":"cannot enter a task-local scope during or after destruction of the underlying thread-local","messagePattern":"cannot enter a task-local scope during or after destruction of the underlying thread-local","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tokio/src/task/task_local.rs","lineNumber":473,"sourceCode":"impl fmt::Display for AccessError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        fmt::Display::fmt(\"task-local value not set\", f)\n    }\n}\n\nimpl Error for AccessError {}\n\nenum ScopeInnerErr {\n    BorrowError,\n    AccessError,\n}\n\nimpl ScopeInnerErr {\n    #[track_caller]\n    fn panic(&self) -> ! {\n        match self {\n            Self::BorrowError => panic!(\"cannot enter a task-local scope while the task-local storage is borrowed\"),\n            Self::AccessError => panic!(\"cannot enter a task-local scope during or after destruction of the underlying thread-local\"),\n        }\n    }\n}\n\nimpl From<std::cell::BorrowMutError> for ScopeInnerErr {\n    fn from(_: std::cell::BorrowMutError) -> Self {\n        Self::BorrowError\n    }\n}\n\nimpl From<std::thread::AccessError> for ScopeInnerErr {\n    fn from(_: std::thread::AccessError) -> Self {\n        Self::AccessError\n    }\n}\n","sourceCodeStart":455,"sourceCodeEnd":489,"githubUrl":"https://github.com/tokio-rs/tokio/blob/108d6d3dc038332af2af83957748333091e35b3f/tokio/src/task/task_local.rs#L455-L489","documentation":"This panic (ScopeInnerErr::AccessError at task_local.rs:473) is raised when scope_inner cannot even reach the RefCell because the underlying thread-local backing storage is being or has been destroyed. In scope_inner (task_local.rs:209) the call self.inner.try_with(...) returns Err(std::thread::AccessError), which is converted into ScopeInnerErr::AccessError and panic()'d at line 473. std::thread::AccessError is what thread-local accessors return once a thread is in its destructor phase (after the thread-local destructors have started running). It means Tokio is being asked to enter a task-local scope on a thread that is already tearing down its thread-local state.","triggerScenarios":"Specifically: (1) a background thread is shutting down and its thread-local destructors are running, while a still-live TaskLocalFuture is being polled or dropped on that thread (the PinnedDrop at task_local.rs:330-342 calls scope_inner during drop); (2) a task whose LocalKey storage lives on a worker thread gets moved or polled after the runtime/worker has started dropping thread-locals; (3) a 'static future spawned on a runtime keeps a TaskLocalFuture alive past the point where the originating thread exited and its thread-locals were freed. The Guard::drop path at task_local.rs:202 (which calls self.local.inner.with) can also surface this during teardown.","commonSituations":"Mixing std::thread::spawn work that holds a TaskLocalFuture across thread exit; spawning blocking work with spawn_blocking that captures task-local futures and outlives the runtime; runtime shutdown while tasks with task-locals are still pending; pinning a scope() future to a thread that is later joined; upgrading Tokio across a version that changed destruction ordering. Common in shutdown/drain code paths and in tests that spawn threads and join them without awaiting outstanding task-local futures.","solutions":["Ensure TaskLocalFuture (and any future produced by LocalKey::scope) is fully driven to completion or dropped before the thread that owns its thread-local storage exits.","Drop task-local scope futures inside a runtime context on a live runtime thread; do not move them into std::thread::spawn threads that then terminate.","During shutdown, await/join all spawned tasks (e.g., shutdown_background(), JoinSet::shutdown after join_all) before letting the runtime drop its worker thread-locals.","If the future must outlive its origin thread, store the scoped value outside the task-local (e.g., pass it explicitly) instead of relying on thread-local-backed storage.","Add a drop guard in your code that flushes/cancels task-local futures before thread::JoinHandle::join returns."],"exampleFix":"// before: thread exits while a TaskLocalFuture is still alive on it\nstd::thread::spawn(|| {\n    let fut = Box::pin(KEY.scope(1, async { work().await }));\n    fut.as_mut().now_or_never(); // maybe Pending\n    // thread returns -> thread-local destructors run\n    // a later drop of `fut` (or a wake) hits scope_inner -> AccessError\n});\n\n// after: drive the future to completion (or drop it) before the thread exits\nstd::thread::spawn(|| {\n    let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();\n    rt.block_on(async {\n        KEY.scope(1, async { work().await }).await; // fully driven here\n    });\n    // thread-local storage torn down only after the future is gone\n});","handlingStrategy":"validation","validationCode":"// This panic only fires when a scope is entered while the underlying\n// thread-local is being/has been destroyed (thread or process teardown).\n// Guard entry points by refusing to scope without a live runtime handle:\nmatch tokio::runtime::Handle::try_current() {\n    Ok(_handle) => KEY.scope(value, fut).await,  // runtime alive -> safe to scope\n    Err(_) => {\n        // Off-runtime or shutting down: don't touch task-local storage here.\n        fut.await\n    }\n}","typeGuard":null,"tryCatchPattern":"use std::panic::{catch_unwind, AssertUnwindSafe};\n\n// Entering a scope during thread-local destruction always panics and there is\n// no Result variant, so catch_unwind is the only runtime backstop. If this\n// fires, your code is running too late in shutdown -- fix the ordering.\nlet r = catch_unwind(AssertUnwindSafe(|| KEY.scope(value, fut)));\nmatch r {\n    Ok(v) => v,\n    Err(p) => eprintln!(\"scope entered during/after thread-local destruction: {p:?}\"),\n}","preventionTips":["Fully `.await` (or explicitly drop) every TaskLocalFuture before dropping the runtime or letting its worker thread exit; in-flight scopes must finish while the thread-local still exists.","Never enter task-local scopes (`scope`/`sync_scope`) or await task-local futures from `Drop` impls that may execute during thread/program shutdown -- destructors run during thread-local destruction.","Shut the runtime down with `shutdown_background`/`shutdown_timeout` only after joining all tasks that hold task-locals; don't drop the runtime while such futures are still pending.","Don't spawn or construct task-local futures from destructors of `static`/global objects; those run during the thread-local teardown phase that triggers this panic.","In any code that might run late in shutdown, prefer `try_with`/`try_get`, which return `Err(AccessError)` instead of panicking when storage is gone."],"tags":["tokio","task-local","thread-local","shutdown","destructor","runtime"],"analyzedSha":"108d6d3dc038332af2af83957748333091e35b3f","analyzedAt":"2026-08-01T08:30:11.576Z","schemaVersion":2}