Skip to content

Unify and simplify command and system error handling #18351

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
Show all changes
35 commits
Select commit Hold shift + click to select a range
0ab0788
Remove App / Subapp helper methods
alice-i-cecile Mar 13, 2025
38baf8f
Remove per-schedule error handling
alice-i-cecile Mar 13, 2025
155ab34
Rename fallible_systems example to error_handling
alice-i-cecile Mar 13, 2025
0894a05
Remove feature flag
alice-i-cecile Mar 13, 2025
45f23b7
Rename error_handler_default for easier grepping
alice-i-cecile Mar 13, 2025
4926938
Move and rename command error-handlers
alice-i-cecile Mar 13, 2025
bc9a808
Move command error handling traits
alice-i-cecile Mar 13, 2025
72b501f
Swap to a generic error context
alice-i-cecile Mar 17, 2025
d3243a1
Unify error handlers and standardize on fn(BevyError, ErrorContext)
alice-i-cecile Mar 17, 2025
0b8bf3b
Standardize on GlobalErrorHandler
alice-i-cecile Mar 17, 2025
66396f2
Small improvements for module docs
alice-i-cecile Mar 17, 2025
38a4915
Import OnceLock for code quality reasons
alice-i-cecile Mar 17, 2025
e5f7832
Helpful traits for ErrorContext
alice-i-cecile Mar 17, 2025
39bec0d
Demonstrate error handling for commands
alice-i-cecile Mar 17, 2025
87f586e
No bevy_ecs imports. Bad!
alice-i-cecile Mar 17, 2025
d52b347
clippy::match_same_arms
alice-i-cecile Mar 17, 2025
09f32d7
OnceLock from bevy_platform_support
alice-i-cecile Mar 17, 2025
a437f80
Missing whitespace in doc comment
alice-i-cecile Mar 17, 2025
2bb2e9c
Yeet default_error_handler
alice-i-cecile Mar 18, 2025
df1f552
Merge branch 'main' into from-the-other-direction
alice-i-cecile Mar 18, 2025
f224aa3
Fix example metadata heading
alice-i-cecile Mar 18, 2025
31b53a9
Regenerate examples README
alice-i-cecile Mar 18, 2025
309aa62
Small fixes for CI
alice-i-cecile Mar 18, 2025
924f365
Merge branch 'main' into from-the-other-direction
alice-i-cecile Mar 18, 2025
e12b996
More doc test fixes
alice-i-cecile Mar 18, 2025
8f030d6
Merge remote-tracking branch 'alice-i-cecile/from-the-other-direction…
alice-i-cecile Mar 18, 2025
b20dc8e
Remove stray mentions of default_error_handler in docs
alice-i-cecile Mar 18, 2025
b987d07
Re-add default error handler helper
alice-i-cecile Mar 18, 2025
83f040a
Re-add and beef up the feature flag
alice-i-cecile Mar 18, 2025
19b90c2
cfg gate the OnceLock import
alice-i-cecile Mar 18, 2025
6e00af7
Ignore doc test that relies on cfg gate :(
alice-i-cecile Mar 18, 2025
fb3ebda
Ensure comments in Cargo.toml are consistent with neighbors
alice-i-cecile Mar 18, 2025
4947b99
Regenerate cargo_features.md
alice-i-cecile Mar 18, 2025
2e7f9d2
Broken doc links
alice-i-cecile Mar 18, 2025
e59acca
Spaces not tabs in doc comments :upside_down:
alice-i-cecile Mar 18, 2025
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
11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ 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 @@ -2208,12 +2211,12 @@ category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "fallible_systems"
path = "examples/ecs/fallible_systems.rs"
name = "error_handling"
path = "examples/ecs/error_handling.rs"
doc-scrape-examples = true
required-features = ["bevy_mesh_picking_backend"]
required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"]

[package.metadata.example.fallible_systems]
[package.metadata.example.error_handling]
name = "Fallible Systems"
description = "Systems that return results to handle errors"
category = "ECS (Entity Component System)"
Expand Down
13 changes: 0 additions & 13 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use alloc::{
pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
error::{BevyError, SystemErrorContext},
event::{event_update_system, EventCursor},
intern::Interned,
prelude::*,
Expand Down Expand Up @@ -1274,18 +1273,6 @@ impl App {
self
}

/// Set the global system error handler to use for systems that return a [`Result`].
///
/// See the [`bevy_ecs::error` module-level documentation](bevy_ecs::error)
/// for more information.
pub fn set_system_error_handler(
&mut self,
error_handler: fn(BevyError, 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: 0 additions & 17 deletions crates/bevy_app/src/sub_app.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::{App, AppLabel, InternedAppLabel, Plugin, Plugins, PluginsState};
use alloc::{boxed::Box, string::String, vec::Vec};
use bevy_ecs::{
error::{DefaultSystemErrorHandler, SystemErrorContext},
event::EventRegistry,
prelude::*,
schedule::{InternedScheduleLabel, InternedSystemSet, ScheduleBuildSettings, ScheduleLabel},
Expand Down Expand Up @@ -336,22 +335,6 @@ impl SubApp {
self
}

/// Set the global error handler to use for systems that return a [`Result`].
///
/// See the [`bevy_ecs::error` module-level documentation](bevy_ecs::error)
/// for more information.
pub fn set_system_error_handler(
&mut self,
error_handler: fn(BevyError, 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
6 changes: 5 additions & 1 deletion crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ 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
## 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
Expand Down
108 changes: 108 additions & 0 deletions crates/bevy_ecs/src/error/command_handling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use core::{any::type_name, fmt};

use crate::{
entity::Entity,
system::{entity_command::EntityCommandError, Command, EntityCommand},
world::{error::EntityMutableFetchError, World},
};

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

/// 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 `()`.
pub trait HandleError<Out = ()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sort of confused why this is a generic with an empty default. The docs don't make it clear to me either. Can you help me out here?

Copy link
Member Author

@alice-i-cecile alice-i-cecile Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved unchanged in this PR. It was introduced in https://github.com/bevyengine/bevy/pull/17215/files#diff-7f622096463e35e3ef38c2c59a68fa4c5e1f0f367632eaf1e9c4779fcc72d74eR69

I would like this to be clearer / better documented, but I'm not convinced this PR is the place for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes more sense to me now that I've read the rest of the code.

/// 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;
/// 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())
}
}

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 {
move |world: &mut World| match self.apply(world) {
Ok(_) => {}
Err(err) => (error_handler)(
err.into(),
ErrorContext::Command {
name: type_name::<C>().into(),
},
),
}
}
}

impl<C> HandleError for C
where
C: Command,
{
#[inline]
fn handle_error_with(self, _error_handler: fn(BevyError, ErrorContext)) -> impl Command {
self
}
#[inline]
fn handle_error(self) -> impl Command
where
Self: Sized,
{
self
}
}

/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that
/// internally runs the [`EntityCommand`] on that entity.
///
// NOTE: This is a separate trait from `EntityCommand` because "result-returning entity commands" and
// "non-result returning entity commands" require different implementations, so they cannot be automatically
// implemented. And this isn't the type of implementation that we want to thrust on people implementing
// EntityCommand.
pub trait CommandWithEntity<Out> {
/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that
/// internally runs the [`EntityCommand`] on that entity.
fn with_entity(self, entity: Entity) -> impl Command<Out> + HandleError<Out>;
}

impl<C> CommandWithEntity<Result<(), EntityMutableFetchError>> for C
where
C: EntityCommand,
{
fn with_entity(
self,
entity: Entity,
) -> impl Command<Result<(), EntityMutableFetchError>>
+ HandleError<Result<(), EntityMutableFetchError>> {
move |world: &mut World| -> Result<(), EntityMutableFetchError> {
let entity = world.get_entity_mut(entity)?;
self.apply(entity);
Ok(())
}
}
}

impl<C, T, Err> CommandWithEntity<Result<T, EntityCommandError<Err>>> for C
where
C: EntityCommand<Result<T, Err>>,
Err: fmt::Debug + fmt::Display + Send + Sync + 'static,
{
fn with_entity(
self,
entity: Entity,
) -> impl Command<Result<T, EntityCommandError<Err>>> + HandleError<Result<T, EntityCommandError<Err>>>
{
move |world: &mut World| {
let entity = world.get_entity_mut(entity)?;
self.apply(entity)
.map_err(EntityCommandError::CommandFailed)
}
}
}
Loading