diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 980174d833bdc..445d2ab396298 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -44,3 +44,7 @@ path = "examples/resources.rs" [[example]] name = "change_detection" path = "examples/change_detection.rs" + +[[example]] +name = "handling_errors_in_systems" +path = "examples/handling_errors_in_systems.rs" diff --git a/crates/bevy_ecs/examples/handling_errors_in_systems.rs b/crates/bevy_ecs/examples/handling_errors_in_systems.rs new file mode 100644 index 0000000000000..9ae807822bb2d --- /dev/null +++ b/crates/bevy_ecs/examples/handling_errors_in_systems.rs @@ -0,0 +1,53 @@ +//! Demonstrates different strategies that might be used to handle systems that could fail. + +use std::error::Error; + +use bevy_ecs::prelude::*; + +fn main() { + let mut world = World::new(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + // This system is fallible, which means it returns a Result. + // If it returns an error, the schedule will panic. + // To see this happen, try changing this to `fallible_system_2`, + // which always returns `Err`. + fallible_system_1, + // To prevent a fallible system from panicking, we can handle + // the error by piping it into another system. + fallible_system_2.pipe(error_handling_system), + // You can also use `.map()` to handle errors. + // Bevy includes a number of built-in functions for handling errors, + // such as `warn` which logs the error using its `Debug` implementation. + fallible_system_2.map(bevy_utils::warn), + // If we don't care about a system failing, we can just ignore the error + // and try again next frame. + fallible_system_2.map(std::mem::drop), + )); + + schedule.run(&mut world); +} + +// A system that might fail. +// A system can only be added to a schedule if it returns nothing, +// or if it returns `Result<(), Error>` with an error type that implements std::fmt::Debug. +// This system always returns `Ok`. +fn fallible_system_1() -> Result<(), Box> { + Ok(()) +} + +// Another fallible system. This one always returns `Err`. +fn fallible_system_2() -> Result<(), Box> { + Err("oops")? +} + +// Our system that we're using to handling errors. +// Our fallible system returns a Result, so we are taking a Result as an input. +fn error_handling_system(In(result): In>>) { + // If the system didn't return an error, we can happily do nothing. + // If it did return an error, we'll just log it and keep going. + if let Err(error) = result { + eprintln!("A system returned an error: {error}"); + } +} diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 76ddc7fbafa32..8efcec60dd320 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use bevy_utils::all_tuples; use crate::{ @@ -6,7 +8,7 @@ use crate::{ graph_utils::{Ambiguity, Dependency, DependencyKind, GraphInfo}, set::{BoxedSystemSet, IntoSystemSet, SystemSet}, }, - system::{BoxedSystem, IntoSystem, System}, + system::{Adapt, AdapterSystem, BoxedSystem, IntoSystem, System}, }; fn new_condition(condition: impl Condition) -> BoxedCondition { @@ -32,7 +34,7 @@ fn ambiguous_with(graph_info: &mut GraphInfo, set: BoxedSystemSet) { } } -impl IntoSystemConfigs for F +impl IntoSystemConfigs<(Marker,)> for F where F: IntoSystem<(), (), Marker>, { @@ -41,6 +43,54 @@ where } } +impl IntoSystemConfigs<(Marker, E)> for F +where + F: IntoSystem<(), Result<(), E>, Marker>, + E: Debug + 'static, +{ + fn into_configs(self) -> SystemConfigs { + struct Unwrap; + + impl Adapt for Unwrap + where + S: System>, + E: Debug, + { + type In = S::In; + type Out = (); + + fn adapt( + &mut self, + input: Self::In, + run_system: impl FnOnce(::In) -> ::Out, + ) -> Self::Out { + Adapt::::adapt_with_name(self, "Unwrap", input, run_system); + } + + fn adapt_with_name( + &mut self, + name: &str, + input: Self::In, + run_system: impl FnOnce(S::In) -> S::Out, + ) -> Self::Out { + if let Err(e) = run_system(input) { + panic_message(name, e); + } + } + } + + #[track_caller] + #[inline(never)] + fn panic_message(name: &str, err: impl Debug) -> ! { + panic!("System '{name}' returned an error. To avoid panicking when this occurs, consider defining an error handling system, and feeding this system's return value into it via `.pipe`. A number of helpful pre-built logging handlers such as `warn` are provided. See the module `bevy_ecs::prelude::system_adapter` for more.\n{err:?}"); + } + + let sys = IntoSystem::into_system(self); + let name = sys.name(); + SystemConfigs::new_system(Box::new(AdapterSystem::new(Unwrap, sys, name))) + } +} + impl IntoSystemConfigs<()> for BoxedSystem<(), ()> { fn into_configs(self) -> SystemConfigs { SystemConfigs::new_system(self) diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 288e91138868b..7e78f8f23748e 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -48,6 +48,19 @@ pub trait Adapt: Send + Sync + 'static { /// When used in an [`AdapterSystem`], this function customizes how the system /// is run and how its inputs/outputs are adapted. fn adapt(&mut self, input: Self::In, run_system: impl FnOnce(S::In) -> S::Out) -> Self::Out; + + /// When used in an [`AdapterSystem`], this function customizes how the system + /// is run and how its inputs/outputs are adapted. + /// Unlike [`Self::adapt`], this method has access to the system's name. + #[allow(unused)] + fn adapt_with_name( + &mut self, + name: &str, + input: Self::In, + run_system: impl FnOnce(S::In) -> S::Out, + ) -> Self::Out { + self.adapt(input, run_system) + } } /// A [`System`] that takes the output of `S` and transforms it by applying `Func` to it. @@ -107,14 +120,15 @@ where #[inline] unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out { // SAFETY: `system.run_unsafe` has the same invariants as `self.run_unsafe`. - self.func - .adapt(input, |input| self.system.run_unsafe(input, world)) + self.func.adapt_with_name(&self.name, input, |input| { + self.system.run_unsafe(input, world) + }) } #[inline] fn run(&mut self, input: Self::In, world: &mut crate::prelude::World) -> Self::Out { self.func - .adapt(input, |input| self.system.run(input, world)) + .adapt_with_name(&self.name, input, |input| self.system.run(input, world)) } #[inline] diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index dab200f032bb5..b8c6785e248a6 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -1803,6 +1803,34 @@ mod tests { run_system(&mut world, || panic!("this system panics")); } + #[test] + fn system_returning_result() { + #[derive(Resource)] + struct ShouldErr(bool); + + fn result_system(flag: Res) -> Result<(), impl std::fmt::Debug> { + if flag.0 { + return Err("Jeepers creepers"); + } + Ok(()) + } + + let mut schedule = Schedule::default(); + schedule.add_systems(result_system); + + let mut world = World::new(); + + // The system will return Ok, so the schedule should not panic. + world.insert_resource(ShouldErr(false)); + schedule.run(&mut world); + + // The system will return Err, so this time the schedule *should* panic. + world.insert_resource(ShouldErr(true)); + let panic = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| schedule.run(&mut world))); + assert!(panic.is_err()); + } + #[test] fn assert_systems() { use std::str::FromStr;