tokio-rs/tokio ยท error
cannot enter a task-local scope while the task-local storage
Error message
cannot enter a task-local scope while the task-local storage is borrowed
What it means
This panic (ScopeInnerErr::BorrowError at task_local.rs:472) is raised when scope_inner fails to enter the task-local scope because the RefCell that backs the task-local's thread-local storage is already borrowed. Concretely, in scope_inner (task_local.rs:209-213) the call self.inner.try_with(|inner| inner.try_borrow_mut()) returns Err(BorrowMutError), which is converted into ScopeInnerErr::BorrowError and eventually panic()'d. Under normal usage this should be impossible: the comment at task_local.rs:192-201 notes user code never receives the RefCell guards, so there is no way for it to leave one outstanding. When it does occur it points to reentrant or concurrent re-borrow of the same LocalKey's storage from within the scope closure.
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 108d6d3dc0)
Solutions
- Audit drop implementations inside the future passed to LocalKey::scope for any re-entrant access to the same LocalKey (e.g., calling KEY.scope(...) or KEY.with(...) from a Drop body) and remove the recursion.
- Ensure only one scope_inner is active on a given LocalKey at a time per thread; serialize nested scope() calls on the same key or use a different task-local key for the inner scope.
- Avoid panicking while a task-local borrow is live; if the closure may panic, isolate the borrow so the guard is released before the panic propagates.
- If the recursion is intentional, switch to thread::scope-style nesting by storing a stack of values in the task-local instead of re-entering scope().
- Reproduce with RUST_BACKTRACE=1 to confirm the reentrant borrow source, then refactor the offending Drop/closure.
Example fix
// before: Drop of an inner future re-enters the same task-local scope
struct ReentrantDrop;
impl Future for ReentrantDrop { type Output = (); /* ... */ }
impl Drop for ReentrantDrop {
fn drop(&mut self) {
// BUG: scope_inner is already borrowing KEY's RefCell here
let _ = KEY.scope(0, async {}).now_or_never();
}
}
// after: do not re-enter the same LocalKey from Drop; read the value if needed
impl Drop for ReentrantDrop {
fn drop(&mut self) {
// only read, do not open a new scope on the same key
let _ = KEY.try_with(|v| *v);
}
} Defensive patterns
Strategy: validation
Validate before calling
// BAD -- enters a new scope while the same key is immutably borrowed -> panic:
// KEY.with(|v| { block_on(KEY.scope(*v, async { /*..*/ })); })
//
// GOOD -- release the borrow BEFORE entering any (possibly nested) scope:
let value: u32 = KEY.with(|v| *v); // borrow dropped at the `;`
KEY.scope(value, async { /* .. */ }).await; // safe: no outstanding borrow Try / catch
use std::panic::{catch_unwind, AssertUnwindSafe};
// scope()/sync_scope() have NO Result-returning variant -- a borrow conflict
// always panics. catch_unwind is the only runtime backstop; restructuring the
// call (see validationCode) is the real fix.
let r = catch_unwind(AssertUnwindSafe(|| KEY.scope(7, async { 7 })));
if r.is_err() {
// A task-local borrow was outstanding when we tried to enter the scope.
// Don't retry in-place: move the scope call outside the `with`/`try_with`.
} Prevention
- Never `.await` a TaskLocalFuture (or call `scope`/`sync_scope`) for a key from inside that same key's `with`/`try_with` closure -- the held immutable `borrow()` blocks the `borrow_mut()` that scope entry needs.
- Keep `with`/`try_with` closures short and synchronous; never span an `.await` point while a borrow is live.
- Clone the value out of the borrow first (`let v = KEY.with(|x| x.clone());`) and then do any async/nested work outside the closure.
- For optional access, prefer `try_with`/`try_get` and bail on `Err(AccessError)` rather than nesting scopes speculatively.
- When nesting scopes on different keys is genuinely required, ensure the inner key's storage is not currently borrowed by any outer closure on the same key.
When it happens
Trigger: Triggered when scope_inner's try_borrow_mut() at task_local.rs:211 fails because the same task-local RefCell is already borrowed. Realistic causes: (1) a panic during scope execution that unwinds while a borrow guard is still live and re-enters scope on the same key; (2) a Drop guard (Guard at task_local.rs:188) running its own scope_inner on the same LocalKey reentrantly while a previous borrow is still active (e.g., the inner future's drop recursively accesses the same task-local via scope()); (3) misuse via unsafe transmute/pin-projection that aliases the slot so two scope_inner calls overlap on the same key. Standard LocalKey::scope/with/try_with usage from a single task will not hit this.
Common situations: Custom AioSource-like code or destructors that re-enter LocalKey::scope during drop of the inner future inside the same scope; downgrading/combining Tokio versions where the borrow bookkeeping differs; unsafe pin projection tricks on TaskLocalFuture; bugs in a runtime that re-uses a LocalKey across logically distinct task-local slots. Rarely seen in idiomatic async code.
Related errors
- `TaskLocalFuture` polled after completion
- cannot enter a task-local scope during or after destruction
- Use AioSource::register_borrowed instead
AI-assisted analysis of tokio-rs/tokio@108d6d3dc0 (2026-08-01).
Data as JSON: /data/errors/74249a45dfed7d7a.json.
Report an issue: GitHub.