Skip to content

Per world error handler #18810

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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,6 @@ bevy_log = ["bevy_internal/bevy_log"]
# Enable input focus subsystem
bevy_input_focus = ["bevy_internal/bevy_input_focus"]

# Use the configurable global error handler as the default error handler.
configurable_error_handler = ["bevy_internal/configurable_error_handler"]

# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]

Expand Down Expand Up @@ -2215,7 +2212,6 @@ wasm = false
name = "fallible_params"
path = "examples/ecs/fallible_params.rs"
doc-scrape-examples = true
required-features = ["configurable_error_handler"]

[package.metadata.example.fallible_params]
name = "Fallible System Parameters"
Expand All @@ -2227,7 +2223,7 @@ wasm = false
name = "error_handling"
path = "examples/ecs/error_handling.rs"
doc-scrape-examples = true
required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"]
required-features = ["bevy_mesh_picking_backend"]

[package.metadata.example.error_handling]
name = "Error handling"
Expand Down
53 changes: 52 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use alloc::{
pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
error::{DefaultErrorHandler, ErrorHandler},
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
Expand Down Expand Up @@ -85,6 +86,7 @@ pub struct App {
/// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html
/// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html
pub(crate) runner: RunnerFn,
default_error_handler: Option<ErrorHandler>,
}

impl Debug for App {
Expand Down Expand Up @@ -143,6 +145,7 @@ impl App {
sub_apps: HashMap::default(),
},
runner: Box::new(run_once),
default_error_handler: None,
}
}

Expand Down Expand Up @@ -1115,7 +1118,12 @@ impl App {
}

/// Inserts a [`SubApp`] with the given label.
pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) {
pub fn insert_sub_app(&mut self, label: impl AppLabel, mut sub_app: SubApp) {
if let Some(handler) = self.default_error_handler {
sub_app
.world_mut()
.get_resource_or_insert_with(|| DefaultErrorHandler(handler));
}
self.sub_apps.sub_apps.insert(label.intern(), sub_app);
}

Expand Down Expand Up @@ -1334,6 +1342,49 @@ impl App {
self.world_mut().add_observer(observer);
self
}

/// Gets the error handler to set for new supapps.
///
/// Note that the error handler of existing subapps may differ.
pub fn get_error_handler(&self) -> Option<ErrorHandler> {
self.default_error_handler
}

/// Set the [default error handler] for the all subapps (including the main one and future ones)
/// that do not have one.
///
/// May only be called once and should be set by the application, not by libraries.
///
/// The handler will be called when an error is produced and not otherwise handled.
///
/// # Panics
/// Panics if called multiple times.
///
/// # Example
/// ```
/// # use bevy_app::*;
/// # use bevy_ecs::error::warn;
/// # fn MyPlugins(_: &mut App) {}
/// App::new()
/// .set_error_handler(warn)
/// .add_plugins(MyPlugins)
/// .run();
/// ```
///
/// [default error handler]: bevy_ecs::error::DefaultErrorHandler
pub fn set_error_handler(&mut self, handler: ErrorHandler) -> &mut Self {
assert!(
self.default_error_handler.is_none(),
"`set_error_handler` called multiple times on same `App`"
);
self.default_error_handler = Some(handler);
for sub_app in self.sub_apps.iter_mut() {
sub_app
.world_mut()
.get_resource_or_insert_with(|| DefaultErrorHandler(handler));
}
self
}
}

type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
Expand Down
7 changes: 0 additions & 7 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ bevy_reflect = ["dep:bevy_reflect"]
## Extends reflection support to functions.
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]

## Use the configurable global error handler as the default error handler.
##
## This is typically used to turn panics from the ECS into loggable errors.
## This may be useful for production builds,
## but can result in a measurable performance impact, especially for commands.
configurable_error_handler = []

## Enables automatic backtrace capturing in BevyError
backtrace = ["std"]

Expand Down
41 changes: 26 additions & 15 deletions crates/bevy_ecs/src/error/command_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,25 @@ use crate::{
world::{error::EntityMutableFetchError, World},
};

use super::{default_error_handler, BevyError, ErrorContext};
use super::{BevyError, ErrorContext, ErrorHandler};

/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
/// Takes a [`Command`] that potentially returns a Result and uses a given error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
pub trait HandleError<Out = ()> {
pub trait HandleError<Out = ()>: Send + 'static {
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command;
fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command;
/// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
fn handle_error(self) -> impl Command
where
Self: Sized,
{
self.handle_error_with(default_error_handler())
}
fn handle_error(self) -> impl Command;
}

impl<C, T, E> HandleError<Result<T, E>> for C
where
C: Command<Result<T, E>>,
E: Into<BevyError>,
{
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command {
fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command {
move |world: &mut World| match self.apply(world) {
Ok(_) => {}
Err(err) => (error_handler)(
Expand All @@ -41,6 +36,18 @@ where
),
}
}

fn handle_error(self) -> impl Command {
move |world: &mut World| match self.apply(world) {
Ok(_) => {}
Err(err) => world.default_error_handler()(
err.into(),
ErrorContext::Command {
name: type_name::<C>().into(),
},
),
}
}
}

impl<C> HandleError<Never> for C
Expand All @@ -52,6 +59,13 @@ where
self.apply(world);
}
}

#[inline]
fn handle_error(self) -> impl Command {
move |world: &mut World| {
self.apply(world);
}
}
}

impl<C> HandleError for C
Expand All @@ -63,10 +77,7 @@ where
self
}
#[inline]
fn handle_error(self) -> impl Command
where
Self: Sized,
{
fn handle_error(self) -> impl Command {
self
}
}
Expand Down
71 changes: 21 additions & 50 deletions crates/bevy_ecs/src/error/handler.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#[cfg(feature = "configurable_error_handler")]
use bevy_platform::sync::OnceLock;
use core::fmt::Display;

use crate::{component::Tick, error::BevyError};
use crate::{component::Tick, error::BevyError, prelude::Resource};
use alloc::borrow::Cow;
use derive_more::derive::{Deref, DerefMut};

/// Context for a [`BevyError`] to aid in debugging.
#[derive(Debug, PartialEq, Eq, Clone)]
Expand Down Expand Up @@ -77,53 +76,6 @@ impl ErrorContext {
}
}

/// A global error handler. This can be set at startup, as long as it is set before
/// any uses. This should generally be configured _before_ initializing the app.
///
/// This should be set inside of your `main` function, before initializing the Bevy app.
/// The value of this error handler can be accessed using the [`default_error_handler`] function,
/// which calls [`OnceLock::get_or_init`] to get the value.
///
/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled!
///
/// # Example
///
/// ```
/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn};
/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally.");
/// // initialize Bevy App here
/// ```
///
/// To use this error handler in your app for custom error handling logic:
///
/// ```rust
/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic};
///
/// fn handle_errors(error: BevyError, ctx: ErrorContext) {
/// let error_handler = default_error_handler();
/// error_handler(error, ctx);
/// }
/// ```
///
/// # Warning
///
/// As this can *never* be overwritten, library code should never set this value.
#[cfg(feature = "configurable_error_handler")]
pub static GLOBAL_ERROR_HANDLER: OnceLock<fn(BevyError, ErrorContext)> = OnceLock::new();

/// The default error handler. This defaults to [`panic()`],
/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization.
/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior,
/// as there may be runtime overhead.
#[inline]
pub fn default_error_handler() -> fn(BevyError, ErrorContext) {
#[cfg(not(feature = "configurable_error_handler"))]
return panic;

#[cfg(feature = "configurable_error_handler")]
return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic);
}

macro_rules! inner {
($call:path, $e:ident, $c:ident) => {
$call!(
Expand All @@ -135,6 +87,25 @@ macro_rules! inner {
};
}

/// Defines how Bevy reacts to errors.
pub type ErrorHandler = fn(BevyError, ErrorContext);

/// Error handler to call when an error is not handled otherwise.
/// Defaults to [`panic()`].
///
/// When updated while a [`Schedule`] is running, it doesn't take effect for
/// that schedule until it's completed.
///
/// [`Schedule`]: crate::schedule::Schedule
#[derive(Resource, Deref, DerefMut, Copy, Clone)]
pub struct DefaultErrorHandler(pub ErrorHandler);

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

/// Error handler that panics with the system error.
#[track_caller]
#[inline]
Expand Down
16 changes: 7 additions & 9 deletions crates/bevy_ecs/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
//! All [`BevyError`]s returned by a system, observer or command 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.
//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app.
//! You can change the default behavior by registering a custom error handler:
//! Use [`DefaultErrorHandler`] to set a custom error handler function for a world,
//! or `App::set_error_handler` for a whole app.
//! In practice, this is generally feature-flagged: panicking or loudly logging errors in development,
//! and quietly logging or ignoring them in production to avoid crashing the app.
//!
Expand All @@ -33,10 +34,8 @@
//! The [`ErrorContext`] allows you to access additional details relevant to providing
//! context surrounding the error – such as the system's [`name`] – in your error messages.
//!
//! Remember to turn on the `configurable_error_handler` feature to set a global error handler!
//!
//! ```rust, ignore
//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext};
//! use bevy_ecs::error::{BevyError, ErrorContext, DefaultErrorHandler};
//! use log::trace;
//!
//! fn my_error_handler(error: BevyError, ctx: ErrorContext) {
Expand All @@ -48,10 +47,9 @@
//! }
//!
//! fn main() {
//! // This requires the "configurable_error_handler" feature to be enabled to be in scope.
//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once.");
//!
//! // Initialize your Bevy App here
//! let mut world = World::new();
//! world.insert_resource(DefaultErrorHandler(my_error_handler));
//! // Use your world here
//! }
//! ```
//!
Expand Down
Loading