Skip to content

Commit 5d0505a

Browse files
Unify and simplify command and system error handling (#18351)
# Objective - ECS error handling is a lovely flagship feature for Bevy 0.16, all in the name of reducing panics and encouraging better error handling (#14275). - Currently though, command and system error handling are completely disjoint and use different mechanisms. - Additionally, there's a number of distinct ways to set the default/fallback/global error handler that have limited value. As far as I can tell, this will be cfg flagged to toggle between dev and production builds in 99.9% of cases, with no real value in more granular settings or helpers. - Fixes #17272 ## Solution - Standardize error handling on the OnceLock global error mechanisms ironed out in #17215 - As discussed there, there are serious performance concerns there, especially for commands - I also think this is a better fit for the use cases, as it's truly global - Move from `SystemErrorContext` to a more general purpose `ErrorContext`, which can handle observers and commands more clearly - Cut the superfluous setter methods on `App` and `SubApp` - Rename the limited (and unhelpful) `fallible_systems` example to `error_handling`, and add an example of command error handling ## Testing Ran the `error_handling` example. ## Notes for reviewers - Do you see a clear way to allow commands to retain &mut World access in the per-command custom error handlers? IMO that's a key feature here (allowing the ad-hoc creation of custom commands), but I'm not sure how to get there without exploding complexity. - I've removed the feature gate on the default_error_handler: contrary to @cart's opinion in #17215 I think that virtually all apps will want to use this. Can you think of a category of app that a) is extremely performance sensitive b) is fine with shipping to production with the panic error handler? If so, I can try to gather performance numbers and/or reintroduce the feature flag. UPDATE: see benches at the end of this message. - ~~`OnceLock` is in `std`: @bushrat011899 what should we do here?~~ - Do you have ideas for more automated tests for this collection of features? ## Benchmarks I checked the impact of the feature flag introduced: benchmarks might show regressions. This bears more investigation. I'm still skeptical that there are users who are well-served by a fast always panicking approach, but I'm going to re-add the feature flag here to avoid stalling this out. ![image](https://github.com/user-attachments/assets/237f644a-b36d-4332-9b45-76fd5cbff4d0) --------- Co-authored-by: Zachary Harrold <[email protected]>
1 parent 65e289f commit 5d0505a

File tree

22 files changed

+385
-365
lines changed

22 files changed

+385
-365
lines changed

Cargo.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ bevy_log = ["bevy_internal/bevy_log"]
280280
# Enable input focus subsystem
281281
bevy_input_focus = ["bevy_internal/bevy_input_focus"]
282282

283+
# Use the configurable global error handler as the default error handler.
284+
configurable_error_handler = ["bevy_internal/configurable_error_handler"]
285+
283286
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
284287
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
285288

@@ -2208,12 +2211,12 @@ category = "ECS (Entity Component System)"
22082211
wasm = false
22092212

22102213
[[example]]
2211-
name = "fallible_systems"
2212-
path = "examples/ecs/fallible_systems.rs"
2214+
name = "error_handling"
2215+
path = "examples/ecs/error_handling.rs"
22132216
doc-scrape-examples = true
2214-
required-features = ["bevy_mesh_picking_backend"]
2217+
required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"]
22152218

2216-
[package.metadata.example.fallible_systems]
2219+
[package.metadata.example.error_handling]
22172220
name = "Fallible Systems"
22182221
description = "Systems that return results to handle errors"
22192222
category = "ECS (Entity Component System)"

crates/bevy_app/src/app.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use alloc::{
1010
pub use bevy_derive::AppLabel;
1111
use bevy_ecs::{
1212
component::RequiredComponentsError,
13-
error::{BevyError, SystemErrorContext},
1413
event::{event_update_system, EventCursor},
1514
intern::Interned,
1615
prelude::*,
@@ -1274,18 +1273,6 @@ impl App {
12741273
self
12751274
}
12761275

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

crates/bevy_app/src/sub_app.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use crate::{App, AppLabel, InternedAppLabel, Plugin, Plugins, PluginsState};
22
use alloc::{boxed::Box, string::String, vec::Vec};
33
use bevy_ecs::{
4-
error::{DefaultSystemErrorHandler, SystemErrorContext},
54
event::EventRegistry,
65
prelude::*,
76
schedule::{InternedScheduleLabel, InternedSystemSet, ScheduleBuildSettings, ScheduleLabel},
@@ -336,22 +335,6 @@ impl SubApp {
336335
self
337336
}
338337

339-
/// Set the global error handler to use for systems that return a [`Result`].
340-
///
341-
/// See the [`bevy_ecs::error` module-level documentation](bevy_ecs::error)
342-
/// for more information.
343-
pub fn set_system_error_handler(
344-
&mut self,
345-
error_handler: fn(BevyError, 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-
355338
/// See [`App::add_event`].
356339
pub fn add_event<T>(&mut self) -> &mut Self
357340
where

crates/bevy_ecs/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ bevy_reflect = ["dep:bevy_reflect"]
3333
## Extends reflection support to functions.
3434
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
3535

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

3943
## Enables automatic backtrace capturing in BevyError
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use core::{any::type_name, fmt};
2+
3+
use crate::{
4+
entity::Entity,
5+
system::{entity_command::EntityCommandError, Command, EntityCommand},
6+
world::{error::EntityMutableFetchError, World},
7+
};
8+
9+
use super::{default_error_handler, BevyError, ErrorContext};
10+
11+
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
12+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
13+
pub trait HandleError<Out = ()> {
14+
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
15+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
16+
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command;
17+
/// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into
18+
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
19+
fn handle_error(self) -> impl Command
20+
where
21+
Self: Sized,
22+
{
23+
self.handle_error_with(default_error_handler())
24+
}
25+
}
26+
27+
impl<C, T, E> HandleError<Result<T, E>> for C
28+
where
29+
C: Command<Result<T, E>>,
30+
E: Into<BevyError>,
31+
{
32+
fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command {
33+
move |world: &mut World| match self.apply(world) {
34+
Ok(_) => {}
35+
Err(err) => (error_handler)(
36+
err.into(),
37+
ErrorContext::Command {
38+
name: type_name::<C>().into(),
39+
},
40+
),
41+
}
42+
}
43+
}
44+
45+
impl<C> HandleError for C
46+
where
47+
C: Command,
48+
{
49+
#[inline]
50+
fn handle_error_with(self, _error_handler: fn(BevyError, ErrorContext)) -> impl Command {
51+
self
52+
}
53+
#[inline]
54+
fn handle_error(self) -> impl Command
55+
where
56+
Self: Sized,
57+
{
58+
self
59+
}
60+
}
61+
62+
/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that
63+
/// internally runs the [`EntityCommand`] on that entity.
64+
///
65+
// NOTE: This is a separate trait from `EntityCommand` because "result-returning entity commands" and
66+
// "non-result returning entity commands" require different implementations, so they cannot be automatically
67+
// implemented. And this isn't the type of implementation that we want to thrust on people implementing
68+
// EntityCommand.
69+
pub trait CommandWithEntity<Out> {
70+
/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that
71+
/// internally runs the [`EntityCommand`] on that entity.
72+
fn with_entity(self, entity: Entity) -> impl Command<Out> + HandleError<Out>;
73+
}
74+
75+
impl<C> CommandWithEntity<Result<(), EntityMutableFetchError>> for C
76+
where
77+
C: EntityCommand,
78+
{
79+
fn with_entity(
80+
self,
81+
entity: Entity,
82+
) -> impl Command<Result<(), EntityMutableFetchError>>
83+
+ HandleError<Result<(), EntityMutableFetchError>> {
84+
move |world: &mut World| -> Result<(), EntityMutableFetchError> {
85+
let entity = world.get_entity_mut(entity)?;
86+
self.apply(entity);
87+
Ok(())
88+
}
89+
}
90+
}
91+
92+
impl<C, T, Err> CommandWithEntity<Result<T, EntityCommandError<Err>>> for C
93+
where
94+
C: EntityCommand<Result<T, Err>>,
95+
Err: fmt::Debug + fmt::Display + Send + Sync + 'static,
96+
{
97+
fn with_entity(
98+
self,
99+
entity: Entity,
100+
) -> impl Command<Result<T, EntityCommandError<Err>>> + HandleError<Result<T, EntityCommandError<Err>>>
101+
{
102+
move |world: &mut World| {
103+
let entity = world.get_entity_mut(entity)?;
104+
self.apply(entity)
105+
.map_err(EntityCommandError::CommandFailed)
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)