bevyengine/bevy · critical

Failed to catch panic!

Error message

Failed to catch panic!

What it means

Inside TaskPool::scope's result collection (task_pool.rs:415-435), each spawned task is awaited; the code expects awaiting a finished task to yield Some(catch_unwind result) and panics with 'Failed to catch panic!' when task.await returns None — i.e. the task completed without producing a result (future dropped/cancelled without being polled to completion or the panic payload was lost). This is an internal invariant of the scope executor, not an application-level error.

Source

Thrown at crates/bevy_tasks/src/task_pool.rs:430

        // SAFETY: As above, all futures must complete in this function so we can change the lifetime
        let scope: &'env Scope<'_, 'env, T> = unsafe { mem::transmute(&scope) };

        f(scope);

        if spawned.is_empty() {
            Vec::new()
        } else {
            block_on(async move {
                let get_results = async {
                    let mut results = Vec::with_capacity(spawned.len());
                    while let Ok(task) = spawned.pop() {
                        if let Some(res) = task.await {
                            match res {
                                Ok(res) => results.push(res),
                                Err(payload) => std::panic::resume_unwind(payload),
                            }
                        } else {
                            panic!("Failed to catch panic!");
                        }
                    }
                    results
                };

                let tick_task_pool_executor = tick_task_pool_executor || self.threads.is_empty();

                // we get this from a thread local so we should always be on the scope executors thread.
                // note: it is possible `scope_executor` and `external_executor` is the same executor,
                // in that case, we should only tick one of them, otherwise, it may cause deadlock.
                let scope_ticker = scope_executor.ticker().unwrap();
                let external_ticker = if !external_executor.is_same(scope_executor) {
                    external_executor.ticker()
                } else {
                    None
                };

                match (external_ticker, tick_task_pool_executor) {

View on GitHub (pinned to 396ca72708)

Solutions

  1. Ensure every closure/future given to TaskPool::scope runs to completion (no early task drop)
  2. Avoid cancelling or abandoning tasks spawned inside a scope
  3. Update Bevy — this path indicates an executor invariant broke; report with a minimal repro
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Futures passed to scope that get dropped before completion; mixing executors or nesting scopes in ways that cancel tasks; engine bugs in the executor shutdown path.

Common situations: Rare; occasionally surfaces during app shutdown while scoped tasks are still queued, or with custom executor wiring around bevy_tasks.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/37b76e7c94c4b786. Report an issue: GitHub.