Skip to content
4 changes: 4 additions & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
53 changes: 53 additions & 0 deletions crates/bevy_ecs/examples/handling_errors_in_systems.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error>> {
Ok(())
}

// Another fallible system. This one always returns `Err`.
fn fallible_system_2() -> Result<(), Box<dyn Error>> {
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<Result<(), Box<dyn Error>>>) {
// 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}");
}
}
54 changes: 52 additions & 2 deletions crates/bevy_ecs/src/schedule/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Debug;

use bevy_utils::all_tuples;

use crate::{
Expand All @@ -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<M>(condition: impl Condition<M>) -> BoxedCondition {
Expand All @@ -32,7 +34,7 @@ fn ambiguous_with(graph_info: &mut GraphInfo, set: BoxedSystemSet) {
}
}

impl<Marker, F> IntoSystemConfigs<Marker> for F
impl<Marker, F> IntoSystemConfigs<(Marker,)> for F
where
F: IntoSystem<(), (), Marker>,
{
Expand All @@ -41,6 +43,54 @@ where
}
}

impl<Marker, F, E> IntoSystemConfigs<(Marker, E)> for F
where
F: IntoSystem<(), Result<(), E>, Marker>,
E: Debug + 'static,
{
fn into_configs(self) -> SystemConfigs {
struct Unwrap;

impl<S, E> Adapt<S> for Unwrap
where
S: System<Out = Result<(), E>>,
E: Debug,
{
type In = S::In;
type Out = ();

fn adapt(
&mut self,
input: Self::In,
run_system: impl FnOnce(<S as System>::In) -> <S as System>::Out,
) -> Self::Out {
Adapt::<S>::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)
Expand Down
20 changes: 17 additions & 3 deletions crates/bevy_ecs/src/system/adapter_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ pub trait Adapt<S: System>: 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.
Expand Down Expand Up @@ -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]
Expand Down
28 changes: 28 additions & 0 deletions crates/bevy_ecs/src/system/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ShouldErr>) -> 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;
Expand Down