{"record":{"id":"37b76e7c94c4b786","repo":"bevyengine/bevy","slug":"failed-to-catch-panic","errorCode":null,"errorMessage":"Failed to catch panic!","messagePattern":"Failed to catch panic!","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/bevy_tasks/src/task_pool.rs","lineNumber":430,"sourceCode":"        // SAFETY: As above, all futures must complete in this function so we can change the lifetime\n        let scope: &'env Scope<'_, 'env, T> = unsafe { mem::transmute(&scope) };\n\n        f(scope);\n\n        if spawned.is_empty() {\n            Vec::new()\n        } else {\n            block_on(async move {\n                let get_results = async {\n                    let mut results = Vec::with_capacity(spawned.len());\n                    while let Ok(task) = spawned.pop() {\n                        if let Some(res) = task.await {\n                            match res {\n                                Ok(res) => results.push(res),\n                                Err(payload) => std::panic::resume_unwind(payload),\n                            }\n                        } else {\n                            panic!(\"Failed to catch panic!\");\n                        }\n                    }\n                    results\n                };\n\n                let tick_task_pool_executor = tick_task_pool_executor || self.threads.is_empty();\n\n                // we get this from a thread local so we should always be on the scope executors thread.\n                // note: it is possible `scope_executor` and `external_executor` is the same executor,\n                // in that case, we should only tick one of them, otherwise, it may cause deadlock.\n                let scope_ticker = scope_executor.ticker().unwrap();\n                let external_ticker = if !external_executor.is_same(scope_executor) {\n                    external_executor.ticker()\n                } else {\n                    None\n                };\n\n                match (external_ticker, tick_task_pool_executor) {","sourceCodeStart":412,"sourceCodeEnd":448,"githubUrl":"https://github.com/bevyengine/bevy/blob/396ca727080776bd313bb892423b7d94e03b81b4/crates/bevy_tasks/src/task_pool.rs#L412-L448","documentation":"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.","triggerScenarios":"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.","commonSituations":"Rare; occasionally surfaces during app shutdown while scoped tasks are still queued, or with custom executor wiring around bevy_tasks.","solutions":["Ensure every closure/future given to TaskPool::scope runs to completion (no early task drop)","Avoid cancelling or abandoning tasks spawned inside a scope","Update Bevy — this path indicates an executor invariant broke; report with a minimal repro"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":["Let every future inside TaskPool::scope run to completion; never drop or abandon scoped tasks","Do not wrap bevy scope tasks in external executors that can cancel them"],"tags":["bevy","task-pool","scope","async","executor","panic"],"backgroundTag":"async-task-panicked","analyzedSha":"396ca727080776bd313bb892423b7d94e03b81b4","analyzedAt":"2026-08-20T16:12:39.808Z","contentChangedAt":"2026-08-20T16:12:39.808Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}