tokio-rs/tokio · critical
cannot enter a task-local scope while the task-local…
Error message
cannot enter a task-local scope while the task-local storage is borrowed
What it means
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).
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.
Example fix
// before — re-entrant scope inside a borrow
NUMBER.with(|_v| {
NUMBER.sync_scope(1, || { /* ... */ }); // panics: borrow held
});
// after — read out, then enter scope with no borrow outstanding
let _v = NUMBER.with(|v| v.clone());
NUMBER.sync_scope(1, || { /* ... */ }); Defensive patterns
Strategy: validation
Validate before calling
// Never call sync_scope/scope from inside a `with`/`try_with` on the SAME key.
// Structural rule; enforce with a helper that reads first, then enters:
fn safe_scope<T: Clone + 'static, R>(
key: &'static tokio::task::LocalKey<T>,
new_value: T,
f: impl FnOnce() -> R,
) -> R {
// no outstanding borrow here -> safe to enter the scope
key.sync_scope(new_value, f)
} Try / catch
// Panics cannot be caught by `?`; use catch_unwind only as a last resort,
// and prefer fixing the re-entrant call site.
std::panic::catch_unwind(|| {
NUMBER.sync_scope(1, || { /* ... */ })
}).ok(); Prevention
- 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.
When it happens
Trigger: 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()`.
Common situations: 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.
Related errors
- cannot enter a task-local scope during or after destruction…
- 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/74249a45dfed7d7a.
Report an issue: GitHub.
Appendix: source
Thrown at tokio/src/task/task_local.rs:472
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)