Skip to content

Commit fd67ca7

Browse files
authored
feat(ecs): configurable error handling for fallible systems (#17753)
You can now configure error handlers for fallible systems. These can be configured on several levels: - Globally via `App::set_systems_error_handler` - Per-schedule via `Schedule::set_error_handler` - Per-system via a piped system (this is existing functionality) The default handler of panicking on error keeps the same behavior as before this commit. The "fallible_systems" example demonstrates the new functionality. This builds on top of #17731, #16589, #17051. --------- Signed-off-by: Jean Mertz <[email protected]>
1 parent c896ad6 commit fd67ca7

File tree

10 files changed

+307
-47
lines changed

10 files changed

+307
-47
lines changed

crates/bevy_app/src/app.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ use bevy_ecs::{
1313
event::{event_update_system, EventCursor},
1414
intern::Interned,
1515
prelude::*,
16+
result::{Error, SystemErrorContext},
1617
schedule::{ScheduleBuildSettings, ScheduleLabel},
1718
system::{IntoObserverSystem, SystemId, SystemInput},
1819
};
1920
use bevy_platform_support::collections::HashMap;
2021
use core::{fmt::Debug, num::NonZero, panic::AssertUnwindSafe};
2122
use log::debug;
22-
use thiserror::Error;
2323

2424
#[cfg(feature = "trace")]
2525
use tracing::info_span;
@@ -44,7 +44,7 @@ pub use bevy_ecs::label::DynEq;
4444
/// A shorthand for `Interned<dyn AppLabel>`.
4545
pub type InternedAppLabel = Interned<dyn AppLabel>;
4646

47-
#[derive(Debug, Error)]
47+
#[derive(Debug, thiserror::Error)]
4848
pub(crate) enum AppError {
4949
#[error("duplicate plugin {plugin_name:?}")]
5050
DuplicatePlugin { plugin_name: String },
@@ -1274,6 +1274,18 @@ impl App {
12741274
self
12751275
}
12761276

1277+
/// Set the global system error handler to use for systems that return a [`Result`].
1278+
///
1279+
/// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html)
1280+
/// for more information.
1281+
pub fn set_system_error_handler(
1282+
&mut self,
1283+
error_handler: fn(Error, SystemErrorContext),
1284+
) -> &mut Self {
1285+
self.main_mut().set_system_error_handler(error_handler);
1286+
self
1287+
}
1288+
12771289
/// Attempts to determine if an [`AppExit`] was raised since the last update.
12781290
///
12791291
/// Will attempt to return the first [`Error`](AppExit::Error) it encounters.

crates/bevy_app/src/sub_app.rs

+17
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use alloc::{boxed::Box, string::String, vec::Vec};
33
use bevy_ecs::{
44
event::EventRegistry,
55
prelude::*,
6+
result::{DefaultSystemErrorHandler, SystemErrorContext},
67
schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel},
78
system::{SystemId, SystemInput},
89
};
@@ -335,6 +336,22 @@ impl SubApp {
335336
self
336337
}
337338

339+
/// Set the global error handler to use for systems that return a [`Result`].
340+
///
341+
/// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html)
342+
/// for more information.
343+
pub fn set_system_error_handler(
344+
&mut self,
345+
error_handler: fn(Error, SystemErrorContext),
346+
) -> &mut Self {
347+
let mut default_handler = self
348+
.world_mut()
349+
.get_resource_or_init::<DefaultSystemErrorHandler>();
350+
351+
default_handler.0 = error_handler;
352+
self
353+
}
354+
338355
/// See [`App::add_event`].
339356
pub fn add_event<T>(&mut self) -> &mut Self
340357
where

crates/bevy_ecs/src/result.rs

+146-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,153 @@
1-
//! Contains error and result helpers for use in fallible systems.
1+
//! Error handling for "fallible" systems.
2+
//!
3+
//! When a system is added to a [`Schedule`], and its return type is that of [`Result`], then Bevy
4+
//! considers those systems to be "fallible", and the ECS scheduler will special-case the [`Err`]
5+
//! variant of the returned `Result`.
6+
//!
7+
//! All [`Error`]s returned by a system are handled by an "error handler". By default, the
8+
//! [`panic`] error handler function is used, resulting in a panic with the error message attached.
9+
//!
10+
//! You can change the default behavior by registering a custom error handler, either globally or
11+
//! per `Schedule`:
12+
//!
13+
//! - [`App::set_system_error_handler`] sets the global error handler for all systems of the
14+
//! current [`World`].
15+
//! - [`Schedule::set_error_handler`] sets the error handler for all systems of that schedule.
16+
//!
17+
//! Bevy provides a number of pre-built error-handlers for you to use:
18+
//!
19+
//! - [`panic`] – panics with the system error
20+
//! - [`error`] – logs the system error at the `error` level
21+
//! - [`warn`] – logs the system error at the `warn` level
22+
//! - [`info`] – logs the system error at the `info` level
23+
//! - [`debug`] – logs the system error at the `debug` level
24+
//! - [`trace`] – logs the system error at the `trace` level
25+
//! - [`ignore`] – ignores the system error
26+
//!
27+
//! However, you can use any custom error handler logic by providing your own function (or
28+
//! non-capturing closure that coerces to the function signature) as long as it matches the
29+
//! signature:
30+
//!
31+
//! ```rust,ignore
32+
//! fn(Error, SystemErrorContext)
33+
//! ```
34+
//!
35+
//! The [`SystemErrorContext`] allows you to access additional details relevant to providing
36+
//! context surrounding the system error – such as the system's [`name`] – in your error messages.
37+
//!
38+
//! For example:
39+
//!
40+
//! ```rust
41+
//! # use bevy_ecs::prelude::*;
42+
//! # use bevy_ecs::schedule::ScheduleLabel;
43+
//! # use log::trace;
44+
//! # fn update() -> Result { Ok(()) }
45+
//! # #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)]
46+
//! # struct MySchedule;
47+
//! # fn main() {
48+
//! let mut schedule = Schedule::new(MySchedule);
49+
//! schedule.add_systems(update);
50+
//! schedule.set_error_handler(|error, ctx| {
51+
//! if ctx.name.ends_with("update") {
52+
//! trace!("Nothing to see here, move along.");
53+
//! return;
54+
//! }
55+
//!
56+
//! bevy_ecs::result::error(error, ctx);
57+
//! });
58+
//! # }
59+
//! ```
60+
//!
61+
//! If you need special handling of individual fallible systems, you can use Bevy's [`system piping
62+
//! feature`] to capture the `Result` output of the system and handle it accordingly.
63+
//!
64+
//! [`Schedule`]: crate::schedule::Schedule
65+
//! [`panic`]: panic()
66+
//! [`World`]: crate::world::World
67+
//! [`Schedule::set_error_handler`]: crate::schedule::Schedule::set_error_handler
68+
//! [`System`]: crate::system::System
69+
//! [`name`]: crate::system::System::name
70+
//! [`App::set_system_error_handler`]: ../../bevy_app/struct.App.html#method.set_system_error_handler
71+
//! [`system piping feature`]: crate::system::In
272
3-
use alloc::boxed::Box;
73+
use crate::{component::Tick, resource::Resource};
74+
use alloc::{borrow::Cow, boxed::Box};
475

576
/// A dynamic error type for use in fallible systems.
677
pub type Error = Box<dyn core::error::Error + Send + Sync + 'static>;
778

879
/// A result type for use in fallible systems.
980
pub type Result<T = (), E = Error> = core::result::Result<T, E>;
81+
82+
/// Additional context for a failed system run.
83+
pub struct SystemErrorContext {
84+
/// The name of the system that failed.
85+
pub name: Cow<'static, str>,
86+
87+
/// The last tick that the system was run.
88+
pub last_run: Tick,
89+
}
90+
91+
/// The default systems error handler stored as a resource in the [`World`](crate::world::World).
92+
pub struct DefaultSystemErrorHandler(pub fn(Error, SystemErrorContext));
93+
94+
impl Resource for DefaultSystemErrorHandler {}
95+
96+
impl Default for DefaultSystemErrorHandler {
97+
fn default() -> Self {
98+
Self(panic)
99+
}
100+
}
101+
102+
macro_rules! inner {
103+
($call:path, $e:ident, $c:ident) => {
104+
$call!("Encountered an error in system `{}`: {:?}", $c.name, $e);
105+
};
106+
}
107+
108+
/// Error handler that panics with the system error.
109+
#[track_caller]
110+
#[inline]
111+
pub fn panic(error: Error, ctx: SystemErrorContext) {
112+
inner!(panic, error, ctx);
113+
}
114+
115+
/// Error handler that logs the system error at the `error` level.
116+
#[track_caller]
117+
#[inline]
118+
pub fn error(error: Error, ctx: SystemErrorContext) {
119+
inner!(log::error, error, ctx);
120+
}
121+
122+
/// Error handler that logs the system error at the `warn` level.
123+
#[track_caller]
124+
#[inline]
125+
pub fn warn(error: Error, ctx: SystemErrorContext) {
126+
inner!(log::warn, error, ctx);
127+
}
128+
129+
/// Error handler that logs the system error at the `info` level.
130+
#[track_caller]
131+
#[inline]
132+
pub fn info(error: Error, ctx: SystemErrorContext) {
133+
inner!(log::info, error, ctx);
134+
}
135+
136+
/// Error handler that logs the system error at the `debug` level.
137+
#[track_caller]
138+
#[inline]
139+
pub fn debug(error: Error, ctx: SystemErrorContext) {
140+
inner!(log::debug, error, ctx);
141+
}
142+
143+
/// Error handler that logs the system error at the `trace` level.
144+
#[track_caller]
145+
#[inline]
146+
pub fn trace(error: Error, ctx: SystemErrorContext) {
147+
inner!(log::trace, error, ctx);
148+
}
149+
150+
/// Error handler that ignores the system error.
151+
#[track_caller]
152+
#[inline]
153+
pub fn ignore(_: Error, _: SystemErrorContext) {}

crates/bevy_ecs/src/schedule/executor/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
component::{ComponentId, Tick},
1919
prelude::{IntoSystemSet, SystemSet},
2020
query::Access,
21-
result::Result,
21+
result::{Error, Result, SystemErrorContext},
2222
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
2323
system::{ScheduleSystem, System, SystemIn},
2424
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
@@ -33,6 +33,7 @@ pub(super) trait SystemExecutor: Send + Sync {
3333
schedule: &mut SystemSchedule,
3434
world: &mut World,
3535
skip_systems: Option<&FixedBitSet>,
36+
error_handler: fn(Error, SystemErrorContext),
3637
);
3738
fn set_apply_final_deferred(&mut self, value: bool);
3839
}

crates/bevy_ecs/src/schedule/executor/multi_threaded.rs

+22-13
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::{
1717
archetype::ArchetypeComponentId,
1818
prelude::Resource,
1919
query::Access,
20+
result::{Error, Result, SystemErrorContext},
2021
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
2122
system::ScheduleSystem,
2223
world::{unsafe_world_cell::UnsafeWorldCell, World},
@@ -131,6 +132,7 @@ pub struct ExecutorState {
131132
struct Context<'scope, 'env, 'sys> {
132133
environment: &'env Environment<'env, 'sys>,
133134
scope: &'scope Scope<'scope, 'env, ()>,
135+
error_handler: fn(Error, SystemErrorContext),
134136
}
135137

136138
impl Default for MultiThreadedExecutor {
@@ -181,6 +183,7 @@ impl SystemExecutor for MultiThreadedExecutor {
181183
schedule: &mut SystemSchedule,
182184
world: &mut World,
183185
_skip_systems: Option<&FixedBitSet>,
186+
error_handler: fn(Error, SystemErrorContext),
184187
) {
185188
let state = self.state.get_mut().unwrap();
186189
// reset counts
@@ -220,7 +223,11 @@ impl SystemExecutor for MultiThreadedExecutor {
220223
false,
221224
thread_executor,
222225
|scope| {
223-
let context = Context { environment, scope };
226+
let context = Context {
227+
environment,
228+
scope,
229+
error_handler,
230+
};
224231

225232
// The first tick won't need to process finished systems, but we still need to run the loop in
226233
// tick_executor() in case a system completes while the first tick still holds the mutex.
@@ -601,17 +608,18 @@ impl ExecutorState {
601608
// access the world data used by the system.
602609
// - `update_archetype_component_access` has been called.
603610
unsafe {
604-
// TODO: implement an error-handling API instead of panicking.
605611
if let Err(err) = __rust_begin_short_backtrace::run_unsafe(
606612
system,
607613
context.environment.world_cell,
608614
) {
609-
panic!(
610-
"Encountered an error in system `{}`: {:?}",
611-
&*system.name(),
612-
err
615+
(context.error_handler)(
616+
err,
617+
SystemErrorContext {
618+
name: system.name(),
619+
last_run: system.get_last_run(),
620+
},
613621
);
614-
};
622+
}
615623
};
616624
}));
617625
context.system_completed(system_index, res, system);
@@ -655,14 +663,15 @@ impl ExecutorState {
655663
// that no other systems currently have access to the world.
656664
let world = unsafe { context.environment.world_cell.world_mut() };
657665
let res = std::panic::catch_unwind(AssertUnwindSafe(|| {
658-
// TODO: implement an error-handling API instead of panicking.
659666
if let Err(err) = __rust_begin_short_backtrace::run(system, world) {
660-
panic!(
661-
"Encountered an error in system `{}`: {:?}",
662-
&*system.name(),
663-
err
667+
(context.error_handler)(
668+
err,
669+
SystemErrorContext {
670+
name: system.name(),
671+
last_run: system.get_last_run(),
672+
},
664673
);
665-
};
674+
}
666675
}));
667676
context.system_completed(system_index, res, system);
668677
};

crates/bevy_ecs/src/schedule/executor/simple.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tracing::info_span;
88
use std::eprintln;
99

1010
use crate::{
11+
result::{Error, SystemErrorContext},
1112
schedule::{
1213
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
1314
},
@@ -43,6 +44,7 @@ impl SystemExecutor for SimpleExecutor {
4344
schedule: &mut SystemSchedule,
4445
world: &mut World,
4546
_skip_systems: Option<&FixedBitSet>,
47+
error_handler: fn(Error, SystemErrorContext),
4648
) {
4749
// If stepping is enabled, make sure we skip those systems that should
4850
// not be run.
@@ -104,12 +106,13 @@ impl SystemExecutor for SimpleExecutor {
104106
}
105107

106108
let f = AssertUnwindSafe(|| {
107-
// TODO: implement an error-handling API instead of panicking.
108109
if let Err(err) = __rust_begin_short_backtrace::run(system, world) {
109-
panic!(
110-
"Encountered an error in system `{}`: {:?}",
111-
&*system.name(),
112-
err
110+
error_handler(
111+
err,
112+
SystemErrorContext {
113+
name: system.name(),
114+
last_run: system.get_last_run(),
115+
},
113116
);
114117
}
115118
});

0 commit comments

Comments
 (0)