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

  1. 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.
  2. 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.
  3. If you genuinely need nested scopes, declare a separate `task_local!` key per nesting level instead of re-entering the same key.
  4. 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

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


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)