Skip to content

feat(ecs): configurable error handling for fallible systems #17753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 11, 2025
16 changes: 14 additions & 2 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ use bevy_ecs::{
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
result::{Error, SystemErrorContext},
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::{IntoObserverSystem, SystemId, SystemInput},
};
use bevy_platform_support::collections::HashMap;
use core::{fmt::Debug, num::NonZero, panic::AssertUnwindSafe};
use log::debug;
use thiserror::Error;

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

#[derive(Debug, Error)]
#[derive(Debug, thiserror::Error)]
pub(crate) enum AppError {
#[error("duplicate plugin {plugin_name:?}")]
DuplicatePlugin { plugin_name: String },
Expand Down Expand Up @@ -1263,6 +1263,18 @@ impl App {
self
}

/// Set the global system error handler to use for systems that return a [`Result`].
///
/// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html)
/// for more information.
pub fn set_system_error_handler(
&mut self,
error_handler: fn(Error, SystemErrorContext),
) -> &mut Self {
self.main_mut().set_system_error_handler(error_handler);
self
}

/// Attempts to determine if an [`AppExit`] was raised since the last update.
///
/// Will attempt to return the first [`Error`](AppExit::Error) it encounters.
Expand Down
17 changes: 17 additions & 0 deletions crates/bevy_app/src/sub_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use alloc::{boxed::Box, string::String, vec::Vec};
use bevy_ecs::{
event::EventRegistry,
prelude::*,
result::{DefaultSystemErrorHandler, SystemErrorContext},
schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel},
system::{SystemId, SystemInput},
};
Expand Down Expand Up @@ -335,6 +336,22 @@ impl SubApp {
self
}

/// Set the global error handler to use for systems that return a [`Result`].
///
/// See the [`bevy_ecs::result` module-level documentation](../../bevy_ecs/result/index.html)
/// for more information.
pub fn set_system_error_handler(
&mut self,
error_handler: fn(Error, SystemErrorContext),
) -> &mut Self {
let mut default_handler = self
.world_mut()
.get_resource_or_init::<DefaultSystemErrorHandler>();

default_handler.0 = error_handler;
self
}

/// See [`App::add_event`].
pub fn add_event<T>(&mut self) -> &mut Self
where
Expand Down
148 changes: 146 additions & 2 deletions crates/bevy_ecs/src/result.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,153 @@
//! Contains error and result helpers for use in fallible systems.
//! Error handling for "fallible" systems.
//!
//! When a system is added to a [`Schedule`], and its return type is that of [`Result`], then Bevy
//! considers those systems to be "fallible", and the ECS scheduler will special-case the [`Err`]
//! variant of the returned `Result`.
//!
//! All [`Error`]s returned by a system are handled by an "error handler". By default, the
//! [`panic`] error handler function is used, resulting in a panic with the error message attached.
//!
//! You can change the default behavior by registering a custom error handler, either globally or
//! per `Schedule`:
//!
//! - [`App::set_system_error_handler`] sets the global error handler for all systems of the
//! current [`World`].
//! - [`Schedule::set_error_handler`] sets the error handler for all systems of that schedule.
//!
//! Bevy provides a number of pre-built error-handlers for you to use:
//!
//! - [`panic`] – panics with the system error
//! - [`error`] – logs the system error at the `error` level
//! - [`warn`] – logs the system error at the `warn` level
//! - [`info`] – logs the system error at the `info` level
//! - [`debug`] – logs the system error at the `debug` level
//! - [`trace`] – logs the system error at the `trace` level
//! - [`ignore`] – ignores the system error
//!
//! However, you can use any custom error handler logic by providing your own function (or
//! non-capturing closure that coerces to the function signature) as long as it matches the
//! signature:
//!
//! ```rust,ignore
//! fn(Error, SystemErrorContext)
//! ```
//!
//! The [`SystemErrorContext`] allows you to access additional details relevant to providing
//! context surrounding the system error – such as the system's [`name`] – in your error messages.
//!
//! For example:
//!
//! ```rust
//! # use bevy_ecs::prelude::*;
//! # use bevy_ecs::schedule::ScheduleLabel;
//! # use log::trace;
//! # fn update() -> Result { Ok(()) }
//! # #[derive(ScheduleLabel, Hash, Debug, PartialEq, Eq, Clone, Copy)]
//! # struct MySchedule;
//! # fn main() {
//! let mut schedule = Schedule::new(MySchedule);
//! schedule.add_systems(update);
//! schedule.set_error_handler(|error, ctx| {
//! if ctx.name.ends_with("update") {
//! trace!("Nothing to see here, move along.");
//! return;
//! }
//!
//! bevy_ecs::result::error(error, ctx);
//! });
//! # }
//! ```
//!
//! If you need special handling of individual fallible systems, you can use Bevy's [`system piping
//! feature`] to capture the `Result` output of the system and handle it accordingly.
//!
//! [`Schedule`]: crate::schedule::Schedule
//! [`panic`]: panic()
//! [`World`]: crate::world::World
//! [`Schedule::set_error_handler`]: crate::schedule::Schedule::set_error_handler
//! [`System`]: crate::system::System
//! [`name`]: crate::system::System::name
//! [`App::set_system_error_handler`]: ../../bevy_app/struct.App.html#method.set_system_error_handler
//! [`system piping feature`]: crate::system::In

use alloc::boxed::Box;
use crate::{component::Tick, resource::Resource};
use alloc::{borrow::Cow, boxed::Box};

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

/// A result type for use in fallible systems.
pub type Result<T = (), E = Error> = core::result::Result<T, E>;

/// Additional context for a failed system run.
pub struct SystemErrorContext {
/// The name of the system that failed.
pub name: Cow<'static, str>,

/// The last tick that the system was run.
pub last_run: Tick,
}

/// The default systems error handler stored as a resource in the [`World`](crate::world::World).
pub struct DefaultSystemErrorHandler(pub fn(Error, SystemErrorContext));

impl Resource for DefaultSystemErrorHandler {}

impl Default for DefaultSystemErrorHandler {
fn default() -> Self {
Self(panic)
}
}

macro_rules! inner {
($call:path, $e:ident, $c:ident) => {
$call!("Encountered an error in system `{}`: {:?}", $c.name, $e);
};
}

/// Error handler that panics with the system error.
#[track_caller]
#[inline]
pub fn panic(error: Error, ctx: SystemErrorContext) {
inner!(panic, error, ctx);
}

/// Error handler that logs the system error at the `error` level.
#[track_caller]
#[inline]
pub fn error(error: Error, ctx: SystemErrorContext) {
inner!(log::error, error, ctx);
}

/// Error handler that logs the system error at the `warn` level.
#[track_caller]
#[inline]
pub fn warn(error: Error, ctx: SystemErrorContext) {
inner!(log::warn, error, ctx);
}

/// Error handler that logs the system error at the `info` level.
#[track_caller]
#[inline]
pub fn info(error: Error, ctx: SystemErrorContext) {
inner!(log::info, error, ctx);
}

/// Error handler that logs the system error at the `debug` level.
#[track_caller]
#[inline]
pub fn debug(error: Error, ctx: SystemErrorContext) {
inner!(log::debug, error, ctx);
}

/// Error handler that logs the system error at the `trace` level.
#[track_caller]
#[inline]
pub fn trace(error: Error, ctx: SystemErrorContext) {
inner!(log::trace, error, ctx);
}

/// Error handler that ignores the system error.
#[track_caller]
#[inline]
pub fn ignore(_: Error, _: SystemErrorContext) {}
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/schedule/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
component::{ComponentId, Tick},
prelude::{IntoSystemSet, SystemSet},
query::Access,
result::Result,
result::{Error, Result, SystemErrorContext},
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
system::{ScheduleSystem, System, SystemIn},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
Expand All @@ -33,6 +33,7 @@ pub(super) trait SystemExecutor: Send + Sync {
schedule: &mut SystemSchedule,
world: &mut World,
skip_systems: Option<&FixedBitSet>,
error_handler: fn(Error, SystemErrorContext),
);
fn set_apply_final_deferred(&mut self, value: bool);
}
Expand Down
35 changes: 22 additions & 13 deletions crates/bevy_ecs/src/schedule/executor/multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
archetype::ArchetypeComponentId,
prelude::Resource,
query::Access,
result::{Error, Result, SystemErrorContext},
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
system::ScheduleSystem,
world::{unsafe_world_cell::UnsafeWorldCell, World},
Expand Down Expand Up @@ -133,6 +134,7 @@ pub struct ExecutorState {
struct Context<'scope, 'env, 'sys> {
environment: &'env Environment<'env, 'sys>,
scope: &'scope Scope<'scope, 'env, ()>,
error_handler: fn(Error, SystemErrorContext),
}

impl Default for MultiThreadedExecutor {
Expand Down Expand Up @@ -183,6 +185,7 @@ impl SystemExecutor for MultiThreadedExecutor {
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: fn(Error, SystemErrorContext),
) {
let state = self.state.get_mut().unwrap();
// reset counts
Expand Down Expand Up @@ -222,7 +225,11 @@ impl SystemExecutor for MultiThreadedExecutor {
false,
thread_executor,
|scope| {
let context = Context { environment, scope };
let context = Context {
environment,
scope,
error_handler,
};

// The first tick won't need to process finished systems, but we still need to run the loop in
// tick_executor() in case a system completes while the first tick still holds the mutex.
Expand Down Expand Up @@ -603,17 +610,18 @@ impl ExecutorState {
// access the world data used by the system.
// - `update_archetype_component_access` has been called.
unsafe {
// TODO: implement an error-handling API instead of panicking.
if let Err(err) = __rust_begin_short_backtrace::run_unsafe(
system,
context.environment.world_cell,
) {
panic!(
"Encountered an error in system `{}`: {:?}",
&*system.name(),
err
(context.error_handler)(
err,
SystemErrorContext {
name: system.name(),
last_run: system.get_last_run(),
},
);
};
}
};
}));
context.system_completed(system_index, res, system);
Expand Down Expand Up @@ -657,14 +665,15 @@ 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(|| {
// TODO: implement an error-handling API instead of panicking.
if let Err(err) = __rust_begin_short_backtrace::run(system, world) {
panic!(
"Encountered an error in system `{}`: {:?}",
&*system.name(),
err
(context.error_handler)(
err,
SystemErrorContext {
name: system.name(),
last_run: system.get_last_run(),
},
);
};
}
}));
context.system_completed(system_index, res, system);
};
Expand Down
13 changes: 8 additions & 5 deletions crates/bevy_ecs/src/schedule/executor/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use tracing::info_span;
use std::eprintln;

use crate::{
result::{Error, SystemErrorContext},
schedule::{
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
},
Expand Down Expand Up @@ -43,6 +44,7 @@ impl SystemExecutor for SimpleExecutor {
schedule: &mut SystemSchedule,
world: &mut World,
_skip_systems: Option<&FixedBitSet>,
error_handler: fn(Error, SystemErrorContext),
) {
// If stepping is enabled, make sure we skip those systems that should
// not be run.
Expand Down Expand Up @@ -104,12 +106,13 @@ impl SystemExecutor for SimpleExecutor {
}

let f = AssertUnwindSafe(|| {
// TODO: implement an error-handling API instead of panicking.
if let Err(err) = __rust_begin_short_backtrace::run(system, world) {
panic!(
"Encountered an error in system `{}`: {:?}",
&*system.name(),
err
error_handler(
err,
SystemErrorContext {
name: system.name(),
last_run: system.get_last_run(),
},
);
}
});
Expand Down
Loading