bevyengine/bevy · critical
Task thread panicked while executing.
Error message
Task thread panicked while executing.
What it means
TaskPool::drop (task_pool.rs:620-631) closes the shutdown channel, joins every worker thread, and expects each join to succeed (skipped only when the dropping thread is already panicking). A worker's main loop wraps task execution in catch_unwind (task_pool.rs:204), so this panic means an unwind escaped that guard — typically from the on_thread_spawn/on_thread_destroy callbacks, thread-local setup/teardown, or executor machinery during shutdown.
Source
Thrown at crates/bevy_tasks/src/task_pool.rs:624
Self::LOCAL_EXECUTOR.with(f)
}
}
impl Default for TaskPool {
fn default() -> Self {
Self::new()
}
}
impl Drop for TaskPool {
fn drop(&mut self) {
self.shutdown_tx.close();
let panicking = thread::panicking();
for join_handle in self.threads.drain(..) {
let res = join_handle.join();
if !panicking {
res.expect("Task thread panicked while executing.");
}
}
}
}
/// A [`TaskPool`] scope for running one or more non-`'static` futures.
///
/// For more information, see [`TaskPool::scope`].
#[derive(Debug)]
pub struct Scope<'scope, 'env: 'scope, T> {
executor: &'scope crate::executor::Executor<'scope>,
external_executor: &'scope ThreadExecutor<'scope>,
scope_executor: &'scope ThreadExecutor<'scope>,
spawned: &'scope ConcurrentQueue<FallibleTask<Result<T, Box<dyn core::any::Any + Send>>>>,
// make `Scope` invariant over 'scope and 'env
scope: PhantomData<&'scope mut &'scope ()>,
env: PhantomData<&'env mut &'env ()>,
}View on GitHub (pinned to 396ca72708)
Solutions
- Make on_thread_spawn/on_thread_destroy callbacks panic-free — return errors/log instead
- Keep task bodies from panicking (return Result) so unwinds never reach worker teardown
- Wrap risky task closures in std::panic::catch_unwind if panics are possible
- Update Bevy and report if no custom callback is involved
Example fix
// before — a panic in the spawn callback kills the worker thread
let pool = TaskPoolBuilder::new()
.on_thread_spawn(|| { panic!("renderer init failed"); })
.build();
// after — log instead of panicking inside pool-owned threads
let pool = TaskPoolBuilder::new()
.on_thread_spawn(|| {
if let Err(e) = init_thread_local_renderer() {
bevy_log::error!("thread init failed: {e}");
}
})
.build(); Defensive patterns
Strategy: try-catch
Try / catch
// keep panics from ever escaping pool-owned threads
let safe_body = std::panic::AssertUnwindSafe(task_body);
match std::panic::catch_unwind(safe_body) {
Ok(result) => result,
Err(payload) => {
bevy_log::error!("task panicked: {payload:?}");
None // recover with a default instead of unwinding the worker
}
} Prevention
- Make on_thread_spawn / on_thread_destroy callbacks infallible (log errors, never panic)
- Prefer returning Result from tasks over asserting inside them
- Install a panic hook that logs payloads so any escaping unwind is diagnosable at pool drop
When it happens
Trigger: A panicking callback passed to TaskPoolBuilder::on_thread_spawn or on_thread_destroy; a panic in executor tick/thread-local destructors at shutdown; aborting the pool while a worker is mid-unwind.
Common situations: Custom thread lifecycle callbacks that panic on init failure; custom executors hooked into bevy task pools; shutdown-ordering issues in embedded Bevy hosts.
Related errors
- Failed to spawn thread.
- Failed to catch panic!
- Cannot call `ReflectComponent::reflect_mut` on component {na
- Cannot call `ReflectComponent::reflect_unchecked_mut` on com
- component should represent a type.
AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20).
Data as JSON: /api/errors/49823738f0d7bd78.
Report an issue: GitHub.