tokio-rs/tokio · error

cannot create LocalSet during thread shutdown

Error message

cannot create LocalSet during thread shutdown

What it means

LocalSet::new() calls context::thread_id().expect('cannot create LocalSet during thread shutdown'). thread_id() returns None when the current thread is in the middle of TLS/thread destruction (pthread exit / thread shutdown phase), so tokio refuses to construct a LocalSet there.

Source

Thrown at tokio/src/task/local.rs:504

                 wake_on_schedule,
             }| {
                ctx.set(self.ctx.take());
                wake_on_schedule.set(self.wake_on_schedule);
            },
        );
    }
}

impl fmt::Debug for LocalEnterGuard {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("LocalEnterGuard").finish()
    }
}

impl LocalSet {
    /// Returns a new local task set.
    pub fn new() -> LocalSet {
        let owner = context::thread_id().expect("cannot create LocalSet during thread shutdown");

        LocalSet {
            tick: Cell::new(0),
            context: Rc::new(Context {
                shared: Arc::new(Shared {
                    local_state: LocalState {
                        owner,
                        owned: LocalOwnedTasks::new(),
                        local_queue: UnsafeCell::new(VecDeque::with_capacity(INITIAL_CAPACITY)),
                    },
                    queue: Mutex::new(Some(VecDeque::with_capacity(INITIAL_CAPACITY))),
                    waker: AtomicWaker::new(),
                    #[cfg(tokio_unstable)]
                    unhandled_panic: crate::runtime::UnhandledPanic::Ignore,
                }),
                unhandled_panic: Cell::new(false),
            }),
            _not_send: PhantomData,

View on GitHub (pinned to 7d0d729d8f)

Solutions

  1. Create the LocalSet eagerly when the thread starts, not in Drop/destructors.
  2. Avoid instantiating LocalSet inside thread-local destructors or atexit handlers.
  3. Guard cleanup code that may run on shutdown from creating new runtimes/LocalSets.
  4. Refactor so the LocalSet lifetime is owned by normal scope, not tied to teardown.

Example fix

// before
thread_local! {
    static LS: LocalSet = LocalSet::new(); // panics if first touch is during shutdown
}
// after
let ls = LocalSet::new(); // created in normal scope
ls.block_on(async { /* ... */ });
Defensive patterns

Strategy: validation

Validate before calling

// Construct the LocalSet in normal scope, never in Drop/destructors.
let ls = tokio::task::LocalSet::new();

Try / catch

// Avoid creating LocalSet during shutdown; catch_unwind only as last resort:
std::panic::catch_unwind(|| tokio::task::LocalSet::new())
    .map_err(|_| io::Error::new(io::ErrorKind::Other, "cannot create LocalSet during shutdown"))?

Prevention

When it happens

Trigger: Constructing LocalSet::new() from a Drop impl, destructor, or atexit/thread-local destructor that runs during thread teardown; spawning a LocalSet from cleanup code on a dying thread.

Common situations: Lazy statics / thread-locals creating a LocalSet on first access during teardown; background thread exiting while another Drop tries to make a LocalSet; libraries that lazily initialize async runtimes in destructors.

Related errors


AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11). Data as JSON: /api/errors/952f63d8e4fe5591. Report an issue: GitHub.