{"record":{"id":"74249a45dfed7d7a","repo":"tokio-rs/tokio","slug":"cannot-enter-a-task-local-scope-while-the-task-loc","errorCode":null,"errorMessage":"cannot enter a task-local scope while the task-local storage is borrowed","messagePattern":"cannot enter a task-local scope while the task-local storage is borrowed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"tokio/src/task/task_local.rs","lineNumber":472,"sourceCode":"\nimpl 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":454,"sourceCodeEnd":489,"githubUrl":"https://github.com/tokio-rs/tokio/blob/625954f365727668cb02d04172b34f1149637728/tokio/src/task/task_local.rs#L454-L489","documentation":"Tokio's task-local storage is backed by a thread-local `RefCell<Option<T>>`. Entering a scope via `LocalKey::sync_scope`/`LocalKey::scope` calls `inner.try_borrow_mut()` (task_local.rs:211) to swap the new value in. That `try_borrow_mut()` fails — yielding `BorrowMutError` → `ScopeInnerErr::BorrowError` — whenever the cell is already borrowed. The panic at line 472 is the documented contract that scope-entry must not happen inside an outstanding `with`/`try_with` on the same key (see the `sync_scope` `# Panics` note at task_local.rs:146).","triggerScenarios":"Calling `KEY.sync_scope(v, ...)` or `KEY.scope(v, fut)` from *inside* an active `KEY.with(|x| ...)` / `KEY.try_with(...)` closure on the same `LocalKey`, because `try_with` holds a `RefCell::borrow()` (task_local.rs:257) for the duration of the closure and the nested scope needs `borrow_mut()`.","commonSituations":"Library helpers that wrap a task-local access and themselves call `sync_scope`; tracing/instrumentation that sets a task-local while a caller already holds a borrow; calling `scope` on the same key from within a future that was itself spawned inside that key's scope while a `with` borrow is live on the stack.","solutions":["Do not call `sync_scope`/`scope` from inside a `KEY.with(...)` or `KEY.try_with(...)` closure on the same `LocalKey` — move the scope entry outside the borrow.","Restructure so the value is read out of `with` (returning a cloned/owned value) and the scope is entered afterward, when no borrow is outstanding.","If you genuinely need nested scopes, declare a separate `task_local!` key per nesting level instead of re-entering the same key.","Ensure no `Ref`/borrow from the task-local is held across the await point where another future on the same thread may enter the scope."],"exampleFix":"// before — re-entrant scope inside a borrow\nNUMBER.with(|_v| {\n    NUMBER.sync_scope(1, || { /* ... */ }); // panics: borrow held\n});\n\n// after — read out, then enter scope with no borrow outstanding\nlet _v = NUMBER.with(|v| v.clone());\nNUMBER.sync_scope(1, || { /* ... */ });","handlingStrategy":"validation","validationCode":"// Never call sync_scope/scope from inside a `with`/`try_with` on the SAME key.\n// Structural rule; enforce with a helper that reads first, then enters:\nfn safe_scope<T: Clone + 'static, R>(\n    key: &'static tokio::task::LocalKey<T>,\n    new_value: T,\n    f: impl FnOnce() -> R,\n) -> R {\n    // no outstanding borrow here -> safe to enter the scope\n    key.sync_scope(new_value, f)\n}","typeGuard":null,"tryCatchPattern":"// Panics cannot be caught by `?`; use catch_unwind only as a last resort,\n// and prefer fixing the re-entrant call site.\nstd::panic::catch_unwind(|| {\n    NUMBER.sync_scope(1, || { /* ... */ })\n}).ok();","preventionTips":["Treat `LocalKey::with`/`try_with` as a read transaction: extract owned/cloned data and return before doing anything that could re-enter the same key.","Never call `sync_scope`/`scope` on a `LocalKey` from within that key's own `with` closure.","In code review, grep for `.scope(` and `.sync_scope(` and verify each is outside any `KEY.with` on the same key.","Add an integration test that exercises the nesting path your production code uses."],"tags":["tokio","task-local","runtime-panic","refcell","reentrancy"],"backgroundTag":null,"analyzedSha":"625954f365727668cb02d04172b34f1149637728","analyzedAt":"2026-08-11T17:46:45.378Z","contentChangedAt":"2026-08-11T17:46:45.378Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}