tokio-rs/tokio · error

Unhandled Panic behavior modified after starting LocalSet

Error message

Unhandled Panic behavior modified after starting LocalSet

What it means

LocalSet::unhandled_panic uses Rc::get_mut(...).and_then(Arc::get_mut).expect(...) to mutate the shared context. Rc::get_mut returns None when there are other strong references to the context — which happens once the LocalSet has started running (it has been cloned into the runtime). Thus configuring unhandled_panic after the LocalSet is running is rejected.

Source

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

        ///     .run_until(async {
        ///         tokio::task::spawn_local(async { panic!("boom"); });
        ///         tokio::task::spawn_local(async {
        ///             // This task never completes
        ///         });
        ///
        ///         // Do some work, but `run_until` will panic before it completes
        /// # loop { tokio::task::yield_now().await; }
        ///     })
        ///     .await;
        /// # }
        /// ```
        ///
        /// [`JoinHandle`]: struct@crate::task::JoinHandle
        pub fn unhandled_panic(&mut self, behavior: crate::runtime::UnhandledPanic) -> &mut Self {
            // TODO: This should be set as a builder
            Rc::get_mut(&mut self.context)
                .and_then(|ctx| Arc::get_mut(&mut ctx.shared))
                .expect("Unhandled Panic behavior modified after starting LocalSet")
                .unhandled_panic = behavior;
            self
        }
    }
}

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

impl Future for LocalSet {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
        let _no_blocking = crate::runtime::context::disallow_block_in_place();

View on GitHub (pinned to 7d0d729d8f)

Solutions

  1. Set unhandled_panic BEFORE spawning any tasks or running the LocalSet.
  2. Configure it immediately after LocalSet::new() while the Rc has a single owner.
  3. If reconfiguration is needed, create a new LocalSet with the desired behavior and migrate.
  4. Treat unhandled_panic as a build-time/construct-time setting, not a runtime toggle.

Example fix

// before
let ls = LocalSet::new();
ls.spawn_local(async { /* ... */ });
ls.unhandled_panic(UnhandledPanic::Shutdown); // panics: already running
// after
let mut ls = LocalSet::new();
ls.unhandled_panic(UnhandledPanic::Shutdown); // before any spawn/run
ls.spawn_local(async { /* ... */ });
Defensive patterns

Strategy: validation

Validate before calling

// Set unhandled_panic before any spawn/run:
let mut ls = tokio::task::LocalSet::new();
ls.unhandled_panic(tokio::runtime::UnhandledPanic::Shutdown);
// only now spawn/run

Try / catch

// No catch needed if you follow the contract; defensive catch_unwind:
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    ls.unhandled_panic(behavior);
})).map_err(|_| io::Error::new(io::ErrorKind::Other, "already running"))?

Prevention

When it happens

Trigger: Calling .unhandled_panic(behavior) on a LocalSet after it has been entered/run via run_until/run/block_on, or after tasks have been spawned onto it.

Common situations: Constructing a LocalSet, spawning tasks, then trying to change unhandled_panic behavior; sharing the LocalSet across structures before configuring it.

Related errors


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