From cbcbd32164afc93d4a8f6bd6f08fcedbbf7a2292 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Sun, 10 May 2026 22:18:50 +0200 Subject: [PATCH 01/13] Pass system panics to fallback error handler --- crates/bevy_ecs/src/error/bevy_error.rs | 28 ++++- .../src/schedule/executor/multi_threaded.rs | 102 ++++++++++++------ .../src/schedule/executor/single_threaded.rs | 22 ++-- 3 files changed, 111 insertions(+), 41 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 15e77b022d535..8ed265daf42c8 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -86,6 +86,30 @@ impl BevyError { Self::from(error).with_severity(severity) } + /// Constructs a new [`BevyError`] with the given [`Severity`]. + /// + /// Like [`BevyError::new`], but if the `backtrace` cargo feature is enabled + /// it will use the supplied backtrace instead of capturing a new one. + pub fn new_with_backtrace( + severity: Severity, + error: E, + backtrace: std::backtrace::Backtrace, + ) -> Self + where + Box: From, + { + #[cfg(not(feature = "backtrace"))] + drop(backtrace); + BevyError { + inner: Box::new(InnerBevyError { + error: error.into(), + severity, + #[cfg(feature = "backtrace")] + backtrace, + }), + } + } + /// Creates a new [`BevyError`] with the [`Severity::Ignore`] severity. /// /// This is a shorthand for [BevyError::new(Severity::Ignore, error)](BevyError::new). @@ -405,9 +429,7 @@ pub fn bevy_error_panic_hook( ) -> impl Fn(&std::panic::PanicHookInfo) { move |info| { if SKIP_NORMAL_BACKTRACE.replace(false) { - if let Some(payload) = info.payload().downcast_ref::<&str>() { - std::println!("{payload}"); - } else if let Some(payload) = info.payload().downcast_ref::() { + if let Some(payload) = info.payload_as_str() { std::println!("{payload}"); } return; diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 197af9df0c078..b53b2c41de5bf 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -7,13 +7,16 @@ use core::{any::Any, panic::AssertUnwindSafe}; use fixedbitset::FixedBitSet; #[cfg(feature = "std")] use std::eprintln; -use std::sync::{Mutex, MutexGuard}; +use std::{ + backtrace::Backtrace, + sync::{Mutex, MutexGuard}, +}; #[cfg(feature = "trace")] use tracing::{info_span, Span}; use crate::{ - error::{ErrorContext, ErrorHandler, Result}, + error::{BevyError, ErrorContext, ErrorHandler, Result, Severity}, prelude::Resource, schedule::{ is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule, SystemWithAccess, @@ -662,22 +665,38 @@ impl ExecutorState { // access the world data used by the system. // - `is_exclusive` returned false unsafe { - if let Err(RunSystemError::Failed(err)) = - __rust_begin_short_backtrace::run_unsafe( - system, - context.environment.world_cell, - ) - { - (context.error_handler)( - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } - }; + __rust_begin_short_backtrace::run_unsafe(system, context.environment.world_cell) + } })); + // If the system returned an unhandled error or panicked, + // invoke the fallback error handler. If the error handler decides to panic, + // we need to catch this panic and rethrow it on the main thread for proper teardown. + let err = match res { + // We can ignore the panic payload here, as it will have already been printed + Err(_) => { + let err = BevyError::new_with_backtrace( + Severity::Panic, + "System panicked", + Backtrace::disabled(), + ); + Some(err) + } + Ok(Err(RunSystemError::Failed(err))) => Some(err), + _ => None, + }; + let res = if let Some(err) = err { + std::panic::catch_unwind(AssertUnwindSafe(|| { + (context.error_handler)( + err, + ErrorContext::System { + name: system.name(), + last_run: system.get_last_run(), + }, + ); + })) + } else { + Ok(()) + }; context.system_completed(system_index, res, system); }; @@ -716,9 +735,22 @@ impl ExecutorState { // that no other systems currently have access to the world. let world = unsafe { context.environment.world_cell.world_mut() }; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - if let Err(RunSystemError::Failed(err)) = - __rust_begin_short_backtrace::run(system, world) - { + __rust_begin_short_backtrace::run(system, world) + })); + let err = match res { + Err(_) => { + let err = BevyError::new_with_backtrace( + Severity::Panic, + "System panicked", + Backtrace::disabled(), + ); + Some(err) + } + Ok(Err(RunSystemError::Failed(err))) => Some(err), + _ => None, + }; + let res = if let Some(err) = err { + std::panic::catch_unwind(AssertUnwindSafe(|| { (context.error_handler)( err, ErrorContext::System { @@ -726,8 +758,10 @@ impl ExecutorState { last_run: system.get_last_run(), }, ); - } - })); + })) + } else { + Ok(()) + }; context.system_completed(system_index, res, system); }; @@ -786,16 +820,22 @@ fn apply_deferred( let res = std::panic::catch_unwind(AssertUnwindSafe(|| { system.apply_deferred(world); })); - if let Err(payload) = res { - #[cfg(feature = "std")] - #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] - { - eprintln!( - "Encountered a panic when applying buffers for system `{}`!", - system.name() + if res.is_err() { + let name = system.name(); + let err = BevyError::new_with_backtrace( + Severity::Panic, + "Encountered a panic while applying system buffers", + Backtrace::disabled(), + ); + std::panic::catch_unwind(AssertUnwindSafe(|| { + world.fallback_error_handler()( + err, + ErrorContext::System { + name, + last_run: system.get_last_run(), + }, ); - } - return Err(payload); + }))?; } } Ok(()) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 6be0a4dd13b6b..947813b093137 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -6,9 +6,6 @@ use alloc::string::ToString as _; #[cfg(feature = "trace")] use tracing::info_span; -#[cfg(feature = "std")] -use std::eprintln; - use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule}, @@ -146,11 +143,22 @@ impl SystemExecutor for SingleThreadedExecutor { }); #[cfg(feature = "std")] - #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { - if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", system.name()); - std::panic::resume_unwind(payload); + if std::panic::catch_unwind(f).is_err() { + use crate::error::{BevyError, Severity}; + + let err = BevyError::new_with_backtrace( + Severity::Panic, + "System panicked", + std::backtrace::Backtrace::disabled(), + ); + error_handler( + err, + ErrorContext::System { + name: system.name(), + last_run: system.get_last_run(), + }, + ); } } From 48d752ff0d2c20340d882f6a4699d90e7a5b62f1 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 01:09:26 +0200 Subject: [PATCH 02/13] Prevent panic thrown by error handler from being turned back into error --- crates/bevy_ecs/src/error/handler.rs | 14 +++++ crates/bevy_ecs/src/schedule/executor/mod.rs | 10 ++++ .../src/schedule/executor/multi_threaded.rs | 60 +++++++++++-------- .../src/schedule/executor/single_threaded.rs | 40 +++++++++++-- 4 files changed, 93 insertions(+), 31 deletions(-) diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index c2ce7a674b1e2..5ca84ba015f1c 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -102,6 +102,11 @@ macro_rules! inner { } /// Defines how Bevy reacts to errors. +/// +/// When writing an error handler, if you want to thow a panic, +/// consider setting [`PANIC_ORIGINATES_FROM_ERROR_HANDLER`]. +/// This lets the excecutor know that a panic doesn't need to be +/// converted back to a [`BevyError`] and passed to the [`FallbackErrorHandler`]. pub type ErrorHandler = fn(BevyError, ErrorContext); /// Fallback error handler to call when an error is not handled otherwise. @@ -124,6 +129,14 @@ impl Default for FallbackErrorHandler { #[deprecated(since = "0.19.0", note = "Renamed to `FallbackErrorHandler`.")] pub type DefaultErrorHandler = FallbackErrorHandler; +#[cfg(feature = "std")] +std::thread_local! { + /// When deliberately throwing a panic in your [`ErrorHandler`], + /// set this to true to indicate to the executor that the panic + /// should not be turned back into a [`BevyError`]. + pub static PANIC_ORIGINATES_FROM_ERROR_HANDLER: core::cell::Cell = const {core::cell::Cell::new(false)}; +} + /// Error handler that defers to an error's [`Severity`]. #[track_caller] #[inline] @@ -143,6 +156,7 @@ pub fn match_severity(err: BevyError, ctx: ErrorContext) { #[track_caller] #[inline] pub fn panic(error: BevyError, ctx: ErrorContext) { + PANIC_ORIGINATES_FROM_ERROR_HANDLER.set(true); inner!(panic, error, ctx); } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index b1c363a87e645..487ab61fa4a6b 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -311,6 +311,16 @@ mod __rust_begin_short_backtrace { // Call `black_box` to prevent this frame from being tail-call optimized away black_box(system.run((), world)) } + + #[inline(never)] + pub(super) fn error_handler( + error_handler: crate::error::ErrorHandler, + err: crate::error::BevyError, + err_context: crate::error::ErrorContext, + ) { + error_handler(err, err_context); + black_box(()); + } } #[cfg(test)] diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index b53b2c41de5bf..1cbd32830a6f5 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -16,7 +16,10 @@ use std::{ use tracing::{info_span, Span}; use crate::{ - error::{BevyError, ErrorContext, ErrorHandler, Result, Severity}, + error::{ + BevyError, ErrorContext, ErrorHandler, Result, Severity, + PANIC_ORIGINATES_FROM_ERROR_HANDLER, + }, prelude::Resource, schedule::{ is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule, SystemWithAccess, @@ -671,32 +674,36 @@ impl ExecutorState { // If the system returned an unhandled error or panicked, // invoke the fallback error handler. If the error handler decides to panic, // we need to catch this panic and rethrow it on the main thread for proper teardown. - let err = match res { - // We can ignore the panic payload here, as it will have already been printed + let (err, mut res) = match res { + // The error has already been handled (by deliberately panicking), so propagate that + Err(payload) if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) => { + (None, Err(payload)) + } + // We can ignore the panic payload here, as it will have already been printed. Err(_) => { let err = BevyError::new_with_backtrace( Severity::Panic, "System panicked", Backtrace::disabled(), ); - Some(err) + (Some(err), Ok(())) } - Ok(Err(RunSystemError::Failed(err))) => Some(err), - _ => None, + Ok(Err(RunSystemError::Failed(err))) => (Some(err), Ok(())), + _ => (None, Ok(())), }; - let res = if let Some(err) = err { - std::panic::catch_unwind(AssertUnwindSafe(|| { - (context.error_handler)( + if let Some(err) = err { + // An error occured, handle it and propagate the panic if needed + res = std::panic::catch_unwind(AssertUnwindSafe(|| { + __rust_begin_short_backtrace::error_handler( + context.error_handler, err, ErrorContext::System { name: system.name(), last_run: system.get_last_run(), }, ); - })) - } else { - Ok(()) - }; + })); + } context.system_completed(system_index, res, system); }; @@ -737,31 +744,30 @@ impl ExecutorState { let res = std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::run(system, world) })); - let err = match res { + let (err, mut res) = match res { Err(_) => { let err = BevyError::new_with_backtrace( Severity::Panic, "System panicked", Backtrace::disabled(), ); - Some(err) + (Some(err), Ok(())) } - Ok(Err(RunSystemError::Failed(err))) => Some(err), - _ => None, + Ok(Err(RunSystemError::Failed(err))) => (Some(err), Ok(())), + _ => (None, Ok(())), }; - let res = if let Some(err) = err { - std::panic::catch_unwind(AssertUnwindSafe(|| { - (context.error_handler)( + if let Some(err) = err { + res = std::panic::catch_unwind(AssertUnwindSafe(|| { + __rust_begin_short_backtrace::error_handler( + context.error_handler, err, ErrorContext::System { name: system.name(), last_run: system.get_last_run(), }, ); - })) - } else { - Ok(()) - }; + })); + } context.system_completed(system_index, res, system); }; @@ -821,7 +827,9 @@ fn apply_deferred( system.apply_deferred(world); })); if res.is_err() { - let name = system.name(); + if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { + return res; + } let err = BevyError::new_with_backtrace( Severity::Panic, "Encountered a panic while applying system buffers", @@ -831,7 +839,7 @@ fn apply_deferred( world.fallback_error_handler()( err, ErrorContext::System { - name, + name: system.name(), last_run: system.get_last_run(), }, ); diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 947813b093137..7240a04a711f3 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -144,15 +144,18 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "std")] { - if std::panic::catch_unwind(f).is_err() { - use crate::error::{BevyError, Severity}; + if let Err(payload) = std::panic::catch_unwind(f) { + if crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { + std::panic::resume_unwind(payload); + } - let err = BevyError::new_with_backtrace( - Severity::Panic, + let err = crate::error::BevyError::new_with_backtrace( + crate::error::Severity::Panic, "System panicked", std::backtrace::Backtrace::disabled(), ); - error_handler( + __rust_begin_short_backtrace::error_handler( + error_handler, err, ErrorContext::System { name: system.name(), @@ -198,7 +201,34 @@ impl SingleThreadedExecutor { fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { for system_index in self.unapplied_systems.ones() { let system = &mut schedule.systems[system_index].system; + #[cfg(not(feature = "std"))] system.apply_deferred(world); + + #[cfg(feature = "std")] + { + let res = + std::panic::catch_unwind(AssertUnwindSafe(|| system.apply_deferred(world))); + + if let Err(payload) = res { + if crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { + std::panic::resume_unwind(payload); + } + + let err = crate::error::BevyError::new_with_backtrace( + crate::error::Severity::Panic, + "System panicked", + std::backtrace::Backtrace::disabled(), + ); + __rust_begin_short_backtrace::error_handler( + world.fallback_error_handler(), + err, + ErrorContext::System { + name: system.name(), + last_run: system.get_last_run(), + }, + ); + } + } } self.unapplied_systems.clear(); From cc1e79a907e8aad5a7f3850b9809f1b6216cfc04 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 10:30:30 +0200 Subject: [PATCH 03/13] tests --- .../src/schedule/executor/multi_threaded.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 1cbd32830a6f5..aa4da51563d82 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -907,7 +907,14 @@ impl MainThreadExecutor { #[cfg(test)] mod tests { + use core::{ + panic::AssertUnwindSafe, + sync::atomic::{AtomicBool, Ordering::Relaxed}, + }; + use std::{panic::catch_unwind, string::String}; + use crate::{ + error::{BevyError, ErrorContext, FallbackErrorHandler}, prelude::Resource, schedule::{IntoScheduleConfigs, MultiThreadedExecutor, Schedule}, system::Commands, @@ -947,4 +954,56 @@ mod tests { schedule.add_systems(((|_: Commands| {}), |_: Commands| {}).chain()); schedule.run(&mut world); } + + #[test] + fn panic_to_error() { + let mut world = World::new(); + + let mut schedule_error = Schedule::default(); + schedule_error.set_executor(MultiThreadedExecutor::new()); + schedule_error.add_systems(|| Err(BevyError::ignore(""))); + + let mut schedule_panic = Schedule::default(); + schedule_panic.set_executor(MultiThreadedExecutor::new()); + schedule_panic.add_systems(|| { + panic!(); + }); + + static HANDLER_CALLED: AtomicBool = AtomicBool::new(false); + fn handle(_: BevyError, ctx: ErrorContext) { + assert!(matches!(ctx, ErrorContext::System { .. })); + HANDLER_CALLED.store(true, Relaxed); + } + world.insert_resource(FallbackErrorHandler(handle)); + + // System error + schedule_error.run(&mut world); + assert!(HANDLER_CALLED.load(Relaxed)); + + // System panic + HANDLER_CALLED.store(false, Relaxed); + schedule_panic.run(&mut world); + assert!(HANDLER_CALLED.load(Relaxed)); + + const PANIC_PAYLOAD: &str = "UwU"; + fn panic(_: BevyError, ctx: ErrorContext) { + assert!(matches!(ctx, ErrorContext::System { .. })); + panic!("{}", PANIC_PAYLOAD); + } + world.insert_resource(FallbackErrorHandler(panic)); + + // System error, handler panic + let result = catch_unwind(AssertUnwindSafe(|| schedule_error.run(&mut world))); + assert_eq!( + *result.unwrap_err().downcast_ref::().unwrap(), + PANIC_PAYLOAD + ); + + // System panic, handler panic + let result = catch_unwind(AssertUnwindSafe(|| schedule_panic.run(&mut world))); + assert_eq!( + *result.unwrap_err().downcast_ref::().unwrap(), + PANIC_PAYLOAD + ); + } } From 42448e33cc329302be0df12e8bbb24d4754bb898 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 12:31:57 +0200 Subject: [PATCH 04/13] typos --- crates/bevy_ecs/src/error/handler.rs | 4 ++-- crates/bevy_ecs/src/schedule/executor/multi_threaded.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index 5ca84ba015f1c..c37feb19d6b47 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -103,9 +103,9 @@ macro_rules! inner { /// Defines how Bevy reacts to errors. /// -/// When writing an error handler, if you want to thow a panic, +/// When writing an error handler, if you want to throw a panic, /// consider setting [`PANIC_ORIGINATES_FROM_ERROR_HANDLER`]. -/// This lets the excecutor know that a panic doesn't need to be +/// This lets the executor know that a panic doesn't need to be /// converted back to a [`BevyError`] and passed to the [`FallbackErrorHandler`]. pub type ErrorHandler = fn(BevyError, ErrorContext); diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index aa4da51563d82..8bbf790915d51 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -692,7 +692,7 @@ impl ExecutorState { _ => (None, Ok(())), }; if let Some(err) = err { - // An error occured, handle it and propagate the panic if needed + // An error occurred, handle it and propagate the panic if needed res = std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::error_handler( context.error_handler, From c34223ab4daf256d47c8a6b8ac2131d25482069e Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 12:39:56 +0200 Subject: [PATCH 05/13] import from alloc in test too --- crates/bevy_ecs/src/schedule/executor/multi_threaded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 8bbf790915d51..23e8bf79b6845 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -911,7 +911,7 @@ mod tests { panic::AssertUnwindSafe, sync::atomic::{AtomicBool, Ordering::Relaxed}, }; - use std::{panic::catch_unwind, string::String}; + use std::{alloc::String, panic::catch_unwind}; use crate::{ error::{BevyError, ErrorContext, FallbackErrorHandler}, From 19664ebdf576ac8c0bf85ac04803ad5550108dba Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 12:43:56 +0200 Subject: [PATCH 06/13] import from alloc in test too --- crates/bevy_ecs/src/schedule/executor/multi_threaded.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 23e8bf79b6845..2e096a640e954 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -907,11 +907,12 @@ impl MainThreadExecutor { #[cfg(test)] mod tests { + use alloc::string::String; use core::{ panic::AssertUnwindSafe, sync::atomic::{AtomicBool, Ordering::Relaxed}, }; - use std::{alloc::String, panic::catch_unwind}; + use std::panic::catch_unwind; use crate::{ error::{BevyError, ErrorContext, FallbackErrorHandler}, From 34a1c43e0c21e3863b3e5a5a8b6c4021c0273794 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 12:57:24 +0200 Subject: [PATCH 07/13] no_std compile --- crates/bevy_ecs/src/error/bevy_error.rs | 1 + crates/bevy_ecs/src/error/handler.rs | 1 + crates/bevy_ecs/src/schedule/executor/multi_threaded.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 8ed265daf42c8..e102970f32231 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -90,6 +90,7 @@ impl BevyError { /// /// Like [`BevyError::new`], but if the `backtrace` cargo feature is enabled /// it will use the supplied backtrace instead of capturing a new one. + #[cfg(feature = "std")] pub fn new_with_backtrace( severity: Severity, error: E, diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index c37feb19d6b47..cfd898c3f5705 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -156,6 +156,7 @@ pub fn match_severity(err: BevyError, ctx: ErrorContext) { #[track_caller] #[inline] pub fn panic(error: BevyError, ctx: ErrorContext) { + #[cfg(feature = "std")] PANIC_ORIGINATES_FROM_ERROR_HANDLER.set(true); inner!(panic, error, ctx); } diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 2e096a640e954..d09f6ce2d3af5 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -967,7 +967,7 @@ mod tests { let mut schedule_panic = Schedule::default(); schedule_panic.set_executor(MultiThreadedExecutor::new()); schedule_panic.add_systems(|| { - panic!(); + panic!("System's panic payload"); }); static HANDLER_CALLED: AtomicBool = AtomicBool::new(false); From f38fb5fe1141515d8746f3f22625d3498c2ea732 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 13:08:42 +0200 Subject: [PATCH 08/13] remove dead code on no_std --- crates/bevy_ecs/src/schedule/executor/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 487ab61fa4a6b..90b1d7afce9cb 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -313,6 +313,7 @@ mod __rust_begin_short_backtrace { } #[inline(never)] + #[cfg(feature = "std")] pub(super) fn error_handler( error_handler: crate::error::ErrorHandler, err: crate::error::BevyError, From 88d2a987d202878d5e2aad54581be28495adf482 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 21:18:32 +0200 Subject: [PATCH 09/13] simplify implementation --- .../src/schedule/executor/multi_threaded.rs | 133 ++++++++---------- .../src/schedule/executor/single_threaded.rs | 84 +++++------ 2 files changed, 100 insertions(+), 117 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index d09f6ce2d3af5..b997783f93000 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -24,7 +24,7 @@ use crate::{ schedule::{ is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule, SystemWithAccess, }, - system::{RunSystemError, ScheduleSystem}, + system::{RunSystemError, ScheduleSystem, System}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; #[cfg(feature = "hotpatching")] @@ -671,39 +671,7 @@ impl ExecutorState { __rust_begin_short_backtrace::run_unsafe(system, context.environment.world_cell) } })); - // If the system returned an unhandled error or panicked, - // invoke the fallback error handler. If the error handler decides to panic, - // we need to catch this panic and rethrow it on the main thread for proper teardown. - let (err, mut res) = match res { - // The error has already been handled (by deliberately panicking), so propagate that - Err(payload) if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) => { - (None, Err(payload)) - } - // We can ignore the panic payload here, as it will have already been printed. - Err(_) => { - let err = BevyError::new_with_backtrace( - Severity::Panic, - "System panicked", - Backtrace::disabled(), - ); - (Some(err), Ok(())) - } - Ok(Err(RunSystemError::Failed(err))) => (Some(err), Ok(())), - _ => (None, Ok(())), - }; - if let Some(err) = err { - // An error occurred, handle it and propagate the panic if needed - res = std::panic::catch_unwind(AssertUnwindSafe(|| { - __rust_begin_short_backtrace::error_handler( - context.error_handler, - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - })); - } + let res = handle_errors(res, context.error_handler, &**system, "System panicked"); context.system_completed(system_index, res, system); }; @@ -744,30 +712,12 @@ impl ExecutorState { let res = std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::run(system, world) })); - let (err, mut res) = match res { - Err(_) => { - let err = BevyError::new_with_backtrace( - Severity::Panic, - "System panicked", - Backtrace::disabled(), - ); - (Some(err), Ok(())) - } - Ok(Err(RunSystemError::Failed(err))) => (Some(err), Ok(())), - _ => (None, Ok(())), - }; - if let Some(err) = err { - res = std::panic::catch_unwind(AssertUnwindSafe(|| { - __rust_begin_short_backtrace::error_handler( - context.error_handler, - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - })); - } + let res = handle_errors( + res, + context.error_handler, + &**system, + "Exclusive system panicked", + ); context.system_completed(system_index, res, system); }; @@ -826,25 +776,12 @@ fn apply_deferred( let res = std::panic::catch_unwind(AssertUnwindSafe(|| { system.apply_deferred(world); })); - if res.is_err() { - if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { - return res; - } - let err = BevyError::new_with_backtrace( - Severity::Panic, - "Encountered a panic while applying system buffers", - Backtrace::disabled(), - ); - std::panic::catch_unwind(AssertUnwindSafe(|| { - world.fallback_error_handler()( - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - }))?; - } + handle_errors( + res.map(|()| Ok(())), + world.fallback_error_handler(), + &**system, + "Encountered a panic while applying system buffers", + )?; } Ok(()) } @@ -888,6 +825,48 @@ unsafe fn evaluate_and_fold_conditions( .fold(true, |acc, res| acc && res) } +/// Handle a potential panic or failed system by invoking the fallback error handler +/// and/or returning a panic payload with which to resume unwinding. +fn handle_errors( + potential_unwind: Result, Box>, + error_handler: ErrorHandler, + in_system: &dyn System, + error_message: &str, +) -> Result<(), Box> { + match potential_unwind { + // A panic occurred, but it came from an error handler, so pass it on to be rethrown + Err(payload) if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) => Err(payload), + // Let the fallback error handler handle the panic, passing on any panic it throws + Err(_) => std::panic::catch_unwind(AssertUnwindSafe(|| { + __rust_begin_short_backtrace::error_handler( + error_handler, + BevyError::new_with_backtrace( + Severity::Panic, + error_message, + Backtrace::disabled(), + ), + ErrorContext::System { + name: in_system.name(), + last_run: in_system.get_last_run(), + }, + ); + })), + // System returned an error, let the fallback error handler handle it, passing on any panic it throws + Ok(Err(RunSystemError::Failed(err))) => std::panic::catch_unwind(AssertUnwindSafe(|| { + __rust_begin_short_backtrace::error_handler( + error_handler, + err, + ErrorContext::System { + name: in_system.name(), + last_run: in_system.get_last_run(), + }, + ); + })), + // Success + _ => Ok(()), + } +} + /// New-typed [`ThreadExecutor`] [`Resource`] that is used to run systems on the main thread #[derive(Resource, Clone)] pub struct MainThreadExecutor(pub Arc>); diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 7240a04a711f3..58b453c1336b6 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -9,7 +9,7 @@ use tracing::info_span; use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule}, - system::{RunSystemError, ScheduleSystem}, + system::{RunSystemError, ScheduleSystem, System}, world::World, }; @@ -144,25 +144,13 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "std")] { - if let Err(payload) = std::panic::catch_unwind(f) { - if crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { - std::panic::resume_unwind(payload); - } - - let err = crate::error::BevyError::new_with_backtrace( - crate::error::Severity::Panic, - "System panicked", - std::backtrace::Backtrace::disabled(), - ); - __rust_begin_short_backtrace::error_handler( - error_handler, - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } + let res = std::panic::catch_unwind(f); + handle_unwind( + res, + world.fallback_error_handler(), + &**system, + "System panicked", + ); } #[cfg(not(feature = "std"))] @@ -208,26 +196,12 @@ impl SingleThreadedExecutor { { let res = std::panic::catch_unwind(AssertUnwindSafe(|| system.apply_deferred(world))); - - if let Err(payload) = res { - if crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { - std::panic::resume_unwind(payload); - } - - let err = crate::error::BevyError::new_with_backtrace( - crate::error::Severity::Panic, - "System panicked", - std::backtrace::Backtrace::disabled(), - ); - __rust_begin_short_backtrace::error_handler( - world.fallback_error_handler(), - err, - ErrorContext::System { - name: system.name(), - last_run: system.get_last_run(), - }, - ); - } + handle_unwind( + res, + world.fallback_error_handler(), + &**system, + "Encountered a panic while applying system buffers", + ); } } @@ -278,3 +252,33 @@ fn evaluate_and_fold_conditions( }) .fold(true, |acc, res| acc && res) } + +/// Handle a potential panic or failed system by invoking the fallback error handler +/// and/or returning a panic payload with which to resume unwinding. +#[cfg(feature = "std")] +fn handle_unwind( + potential_unwind: Result<(), alloc::boxed::Box>, + error_handler: ErrorHandler, + in_system: &dyn System, + error_message: &str, +) { + if let Err(payload) = potential_unwind { + if crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) { + std::panic::resume_unwind(payload); + } + + let err = crate::error::BevyError::new_with_backtrace( + crate::error::Severity::Panic, + error_message, + std::backtrace::Backtrace::disabled(), + ); + __rust_begin_short_backtrace::error_handler( + error_handler, + err, + ErrorContext::System { + name: in_system.name(), + last_run: in_system.get_last_run(), + }, + ); + } +} From 401d514f683e6e1ae061c4ff66d597f8106553d1 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 22:25:41 +0200 Subject: [PATCH 10/13] use provided error handler --- .../src/schedule/executor/multi_threaded.rs | 18 ++++++++++----- .../src/schedule/executor/single_threaded.rs | 23 +++++++++---------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index b997783f93000..ede991643c032 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -300,7 +300,7 @@ impl SystemExecutor for MultiThreadedExecutor { if self.apply_final_deferred { // Do one final apply buffers after all systems have completed // Commands should be applied while on the scope's thread, not the executor's thread - let res = apply_deferred(&state.unapplied_systems, systems, world); + let res = apply_deferred(&state.unapplied_systems, systems, world, error_handler); if let Err(payload) = res { let panic_payload = self.panic_payload.get_mut().unwrap(); *panic_payload = Some(payload); @@ -699,7 +699,12 @@ impl ExecutorState { // SAFETY: `can_run` returned true for this system, which means // that no other systems currently have access to the world. let world = unsafe { context.environment.world_cell.world_mut() }; - let res = apply_deferred(&unapplied_systems, context.environment.systems, world); + let res = apply_deferred( + &unapplied_systems, + context.environment.systems, + world, + context.error_handler, + ); context.system_completed(system_index, res, system); }; @@ -769,6 +774,7 @@ fn apply_deferred( unapplied_systems: &FixedBitSet, systems: &[SyncUnsafeCell], world: &mut World, + error_handler: ErrorHandler, ) -> Result<(), Box> { for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist @@ -778,7 +784,7 @@ fn apply_deferred( })); handle_errors( res.map(|()| Ok(())), - world.fallback_error_handler(), + error_handler, &**system, "Encountered a panic while applying system buffers", )?; @@ -825,7 +831,7 @@ unsafe fn evaluate_and_fold_conditions( .fold(true, |acc, res| acc && res) } -/// Handle a potential panic or failed system by invoking the fallback error handler +/// Handle a potential panic or failed system by invoking the error handler /// and/or returning a panic payload with which to resume unwinding. fn handle_errors( potential_unwind: Result, Box>, @@ -836,7 +842,7 @@ fn handle_errors( match potential_unwind { // A panic occurred, but it came from an error handler, so pass it on to be rethrown Err(payload) if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) => Err(payload), - // Let the fallback error handler handle the panic, passing on any panic it throws + // Let the error handler handle the panic, passing on any panic it throws Err(_) => std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::error_handler( error_handler, @@ -851,7 +857,7 @@ fn handle_errors( }, ); })), - // System returned an error, let the fallback error handler handle it, passing on any panic it throws + // System returned an error, let the error handler handle it, passing on any panic it throws Ok(Err(RunSystemError::Failed(err))) => std::panic::catch_unwind(AssertUnwindSafe(|| { __rust_begin_short_backtrace::error_handler( error_handler, diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 58b453c1336b6..9e7bf5bdb1bf1 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -124,7 +124,7 @@ impl SystemExecutor for SingleThreadedExecutor { } if is_apply_deferred(&**system) { - self.apply_deferred(schedule, world); + self.apply_deferred(schedule, world, error_handler); continue; } @@ -145,12 +145,7 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "std")] { let res = std::panic::catch_unwind(f); - handle_unwind( - res, - world.fallback_error_handler(), - &**system, - "System panicked", - ); + handle_unwind(res, error_handler, &**system, "System panicked"); } #[cfg(not(feature = "std"))] @@ -162,7 +157,7 @@ impl SystemExecutor for SingleThreadedExecutor { } if self.apply_final_deferred { - self.apply_deferred(schedule, world); + self.apply_deferred(schedule, world, error_handler); } self.evaluated_sets.clear(); self.completed_systems.clear(); @@ -186,7 +181,12 @@ impl SingleThreadedExecutor { } } - fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + fn apply_deferred( + &mut self, + schedule: &mut SystemSchedule, + world: &mut World, + error_handler: ErrorHandler, + ) { for system_index in self.unapplied_systems.ones() { let system = &mut schedule.systems[system_index].system; #[cfg(not(feature = "std"))] @@ -198,7 +198,7 @@ impl SingleThreadedExecutor { std::panic::catch_unwind(AssertUnwindSafe(|| system.apply_deferred(world))); handle_unwind( res, - world.fallback_error_handler(), + error_handler, &**system, "Encountered a panic while applying system buffers", ); @@ -253,8 +253,7 @@ fn evaluate_and_fold_conditions( .fold(true, |acc, res| acc && res) } -/// Handle a potential panic or failed system by invoking the fallback error handler -/// and/or returning a panic payload with which to resume unwinding. +/// Handle a potential panic by invoking the error handler #[cfg(feature = "std")] fn handle_unwind( potential_unwind: Result<(), alloc::boxed::Box>, From 321e3f176f9bdf6f605826f5195ab37dced6c120 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 22:38:33 +0200 Subject: [PATCH 11/13] no unused on no_std --- crates/bevy_ecs/src/schedule/executor/single_threaded.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 9e7bf5bdb1bf1..a39116b233fa2 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -9,7 +9,7 @@ use tracing::info_span; use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{is_apply_deferred, ConditionWithAccess, SystemExecutor, SystemSchedule}, - system::{RunSystemError, ScheduleSystem, System}, + system::{RunSystemError, ScheduleSystem}, world::World, }; @@ -190,7 +190,10 @@ impl SingleThreadedExecutor { for system_index in self.unapplied_systems.ones() { let system = &mut schedule.systems[system_index].system; #[cfg(not(feature = "std"))] - system.apply_deferred(world); + { + system.apply_deferred(world); + let _ = error_handler; + } #[cfg(feature = "std")] { @@ -258,7 +261,7 @@ fn evaluate_and_fold_conditions( fn handle_unwind( potential_unwind: Result<(), alloc::boxed::Box>, error_handler: ErrorHandler, - in_system: &dyn System, + in_system: &dyn crate::system::System, error_message: &str, ) { if let Err(payload) = potential_unwind { From b646d413e3dbaec68c2de5e89867dce99982c5fc Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 22:58:55 +0200 Subject: [PATCH 12/13] test: unify payload recovery --- .../src/schedule/executor/multi_threaded.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index ede991643c032..d0e1fe6e17f26 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -892,8 +892,10 @@ impl MainThreadExecutor { #[cfg(test)] mod tests { + use alloc::boxed::Box; use alloc::string::String; use core::{ + any::Any, panic::AssertUnwindSafe, sync::atomic::{AtomicBool, Ordering::Relaxed}, }; @@ -972,6 +974,16 @@ mod tests { assert!(HANDLER_CALLED.load(Relaxed)); const PANIC_PAYLOAD: &str = "UwU"; + fn assert_panic_payload(result: Result<(), Box>) { + let payload = result.unwrap_err(); + assert_eq!( + payload + .downcast_ref::() + .map(String::as_str) + .unwrap_or_else(|| payload.downcast_ref::<&str>().unwrap()), + PANIC_PAYLOAD + ); + } fn panic(_: BevyError, ctx: ErrorContext) { assert!(matches!(ctx, ErrorContext::System { .. })); panic!("{}", PANIC_PAYLOAD); @@ -980,16 +992,10 @@ mod tests { // System error, handler panic let result = catch_unwind(AssertUnwindSafe(|| schedule_error.run(&mut world))); - assert_eq!( - *result.unwrap_err().downcast_ref::().unwrap(), - PANIC_PAYLOAD - ); + assert_panic_payload(result); // System panic, handler panic let result = catch_unwind(AssertUnwindSafe(|| schedule_panic.run(&mut world))); - assert_eq!( - *result.unwrap_err().downcast_ref::().unwrap(), - PANIC_PAYLOAD - ); + assert_panic_payload(result); } } From 1a55046958faf04c11b2f2a6fe444d1ade97ff50 Mon Sep 17 00:00:00 2001 From: Mira Morgana Date: Mon, 11 May 2026 23:25:36 +0200 Subject: [PATCH 13/13] Reset panic origin flag in case of an outer catch_unwind --- .../src/schedule/executor/multi_threaded.rs | 93 +++++++++++-------- .../src/schedule/executor/single_threaded.rs | 1 + 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index d0e1fe6e17f26..3e57f3ed3d41e 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -662,16 +662,23 @@ impl ExecutorState { let system_meta = &self.system_task_metadata[system_index]; let task = async move { - let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - // SAFETY: - // - The caller ensures that we have permission to - // access the world data used by the system. - // - `is_exclusive` returned false - unsafe { - __rust_begin_short_backtrace::run_unsafe(system, context.environment.world_cell) - } - })); - let res = handle_errors(res, context.error_handler, &**system, "System panicked"); + let res = handle_errors( + |system| { + // SAFETY: + // - The caller ensures that we have permission to + // access the world data used by the system. + // - `is_exclusive` returned false + unsafe { + __rust_begin_short_backtrace::run_unsafe( + system, + context.environment.world_cell, + ) + } + }, + system, + context.error_handler, + "System panicked", + ); context.system_completed(system_index, res, system); }; @@ -714,13 +721,10 @@ impl ExecutorState { // SAFETY: `can_run` returned true for this system, which means // that no other systems currently have access to the world. let world = unsafe { context.environment.world_cell.world_mut() }; - let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - __rust_begin_short_backtrace::run(system, world) - })); let res = handle_errors( - res, + |system| __rust_begin_short_backtrace::run(system, world), + system, context.error_handler, - &**system, "Exclusive system panicked", ); context.system_completed(system_index, res, system); @@ -779,13 +783,13 @@ fn apply_deferred( for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist let system = &mut unsafe { &mut *systems[system_index].get() }.system; - let res = std::panic::catch_unwind(AssertUnwindSafe(|| { - system.apply_deferred(world); - })); handle_errors( - res.map(|()| Ok(())), + |system| { + system.apply_deferred(world); + Ok(()) + }, + system, error_handler, - &**system, "Encountered a panic while applying system buffers", )?; } @@ -834,11 +838,13 @@ unsafe fn evaluate_and_fold_conditions( /// Handle a potential panic or failed system by invoking the error handler /// and/or returning a panic payload with which to resume unwinding. fn handle_errors( - potential_unwind: Result, Box>, + f: impl FnOnce(&mut Box>) -> Result<(), RunSystemError>, + system: &mut Box>, error_handler: ErrorHandler, - in_system: &dyn System, error_message: &str, ) -> Result<(), Box> { + PANIC_ORIGINATES_FROM_ERROR_HANDLER.set(false); + let potential_unwind = std::panic::catch_unwind(AssertUnwindSafe(|| f(system))); match potential_unwind { // A panic occurred, but it came from an error handler, so pass it on to be rethrown Err(payload) if PANIC_ORIGINATES_FROM_ERROR_HANDLER.replace(false) => Err(payload), @@ -852,8 +858,8 @@ fn handle_errors( Backtrace::disabled(), ), ErrorContext::System { - name: in_system.name(), - last_run: in_system.get_last_run(), + name: system.name(), + last_run: system.get_last_run(), }, ); })), @@ -863,8 +869,8 @@ fn handle_errors( error_handler, err, ErrorContext::System { - name: in_system.name(), - last_run: in_system.get_last_run(), + name: system.name(), + last_run: system.get_last_run(), }, ); })), @@ -892,17 +898,17 @@ impl MainThreadExecutor { #[cfg(test)] mod tests { - use alloc::boxed::Box; use alloc::string::String; use core::{ - any::Any, panic::AssertUnwindSafe, sync::atomic::{AtomicBool, Ordering::Relaxed}, }; use std::panic::catch_unwind; use crate::{ - error::{BevyError, ErrorContext, FallbackErrorHandler}, + error::{ + BevyError, ErrorContext, FallbackErrorHandler, PANIC_ORIGINATES_FROM_ERROR_HANDLER, + }, prelude::Resource, schedule::{IntoScheduleConfigs, MultiThreadedExecutor, Schedule}, system::Commands, @@ -974,28 +980,33 @@ mod tests { assert!(HANDLER_CALLED.load(Relaxed)); const PANIC_PAYLOAD: &str = "UwU"; - fn assert_panic_payload(result: Result<(), Box>) { - let payload = result.unwrap_err(); - assert_eq!( - payload - .downcast_ref::() - .map(String::as_str) - .unwrap_or_else(|| payload.downcast_ref::<&str>().unwrap()), - PANIC_PAYLOAD - ); - } fn panic(_: BevyError, ctx: ErrorContext) { assert!(matches!(ctx, ErrorContext::System { .. })); + PANIC_ORIGINATES_FROM_ERROR_HANDLER.set(true); panic!("{}", PANIC_PAYLOAD); } world.insert_resource(FallbackErrorHandler(panic)); // System error, handler panic let result = catch_unwind(AssertUnwindSafe(|| schedule_error.run(&mut world))); - assert_panic_payload(result); + let payload = result.unwrap_err(); + assert_eq!( + payload + .downcast_ref::() + .map(String::as_str) + .unwrap_or_else(|| payload.downcast_ref::<&str>().unwrap()), + PANIC_PAYLOAD + ); // System panic, handler panic let result = catch_unwind(AssertUnwindSafe(|| schedule_panic.run(&mut world))); - assert_panic_payload(result); + let payload = result.unwrap_err(); + assert_eq!( + payload + .downcast_ref::() + .map(String::as_str) + .unwrap_or_else(|| payload.downcast_ref::<&str>().unwrap()), + PANIC_PAYLOAD + ); } } diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index a39116b233fa2..5f90c1859db79 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -197,6 +197,7 @@ impl SingleThreadedExecutor { #[cfg(feature = "std")] { + crate::error::PANIC_ORIGINATES_FROM_ERROR_HANDLER.set(false); let res = std::panic::catch_unwind(AssertUnwindSafe(|| system.apply_deferred(world))); handle_unwind(