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
- Set unhandled_panic BEFORE spawning any tasks or running the LocalSet.
- Configure it immediately after LocalSet::new() while the Rc has a single owner.
- If reconfiguration is needed, create a new LocalSet with the desired behavior and migrate.
- 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
- Configure unhandled_panic immediately after new().
- Never mutate it after spawn_local/run_until/block_on.
- Treat it as a build-time setting.
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
- There must be more than one worker
- cannot create LocalSet during thread shutdown
- there is no signal driver running, must be called from the c
- task panicked
- `JoinError` reason is not a panic.
AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11).
Data as JSON: /api/errors/91dfd597a45d8c3e.
Report an issue: GitHub.