Skip to content

Commit 326a496

Browse files
committed
Update the ecs error handler to allow being updated more than once
1 parent 18e1bf1 commit 326a496

File tree

11 files changed

+125
-78
lines changed

11 files changed

+125
-78
lines changed

crates/bevy_ecs/src/error/command_handling.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
world::{error::EntityMutableFetchError, World},
88
};
99

10-
use super::{default_error_handler, BevyError, ErrorContext};
10+
use super::{get_error_handler, BevyError, ErrorContext};
1111

1212
/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into
1313
/// a [`Command`] that internally handles an error if it occurs and returns `()`.
@@ -21,7 +21,7 @@ pub trait HandleError<Out = ()> {
2121
where
2222
Self: Sized,
2323
{
24-
self.handle_error_with(default_error_handler())
24+
self.handle_error_with(get_error_handler())
2525
}
2626
}
2727

crates/bevy_ecs/src/error/handler.rs

Lines changed: 95 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#[cfg(feature = "configurable_error_handler")]
2-
use bevy_platform::sync::OnceLock;
31
use core::fmt::Display;
42

53
use crate::{component::Tick, error::BevyError};
@@ -77,53 +75,67 @@ impl ErrorContext {
7775
}
7876
}
7977

80-
/// A global error handler. This can be set at startup, as long as it is set before
81-
/// any uses. This should generally be configured _before_ initializing the app.
82-
///
83-
/// This should be set inside of your `main` function, before initializing the Bevy app.
84-
/// The value of this error handler can be accessed using the [`default_error_handler`] function,
85-
/// which calls [`OnceLock::get_or_init`] to get the value.
86-
///
87-
/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled!
88-
///
89-
/// # Example
90-
///
91-
/// ```
92-
/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn};
93-
/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally.");
94-
/// // initialize Bevy App here
95-
/// ```
96-
///
97-
/// To use this error handler in your app for custom error handling logic:
98-
///
99-
/// ```rust
100-
/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic};
101-
///
102-
/// fn handle_errors(error: BevyError, ctx: ErrorContext) {
103-
/// let error_handler = default_error_handler();
104-
/// error_handler(error, ctx);
105-
/// }
106-
/// ```
107-
///
108-
/// # Warning
109-
///
110-
/// As this can *never* be overwritten, library code should never set this value.
78+
type BevyErrorHandler = fn(BevyError, ErrorContext);
79+
11180
#[cfg(feature = "configurable_error_handler")]
112-
pub static GLOBAL_ERROR_HANDLER: OnceLock<fn(BevyError, ErrorContext)> = OnceLock::new();
81+
mod inner {
82+
use super::*;
83+
use core::sync::atomic::{AtomicPtr, Ordering};
11384

114-
/// The default error handler. This defaults to [`panic()`],
115-
/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization.
116-
/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior,
117-
/// as there may be runtime overhead.
118-
#[inline]
119-
pub fn default_error_handler() -> fn(BevyError, ErrorContext) {
120-
#[cfg(not(feature = "configurable_error_handler"))]
121-
return panic;
85+
// TODO: If we're willing to stomach the perf cost we could do a `RwLock<Box<dyn Fn(..)>>`.
86+
static GLOBAL_ERROR_HANDLER: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
12287

123-
#[cfg(feature = "configurable_error_handler")]
124-
return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic);
88+
/// Gets the global error handler.
89+
///
90+
/// If not set by [`set_error_handler`] defaults to [`panic()`].
91+
pub fn get_error_handler() -> BevyErrorHandler {
92+
// We need the acquire ordering since a sufficiently malicious user might call `set_error_handler`
93+
// and immediately call `get_error_handler` so we need to make sure these loads and stores are synchronized
94+
// with each other.
95+
let handler = GLOBAL_ERROR_HANDLER.load(Ordering::Acquire);
96+
97+
if handler.is_null() {
98+
panic
99+
} else {
100+
// SAFETY: We just checked if this is null and the only way to set this value is using `set_error_handler` which
101+
// makes sure this is actually a `BevyErrorHandler`.
102+
unsafe { core::mem::transmute::<*mut (), BevyErrorHandler>(handler) }
103+
}
104+
}
105+
106+
/// Sets the error handler.
107+
///
108+
/// This function is only available with the `configurable_error_handler` method enabled.
109+
pub fn set_error_handler(hook: BevyErrorHandler) {
110+
// Casting function pointers to normal pointers and back is called out as non-portable
111+
// by the `mem::transmute` documentation. The problem is that on some architectures
112+
// the size of a function pointers might be different from the size of a normal pointer.
113+
//
114+
// As of 2025-04-20 we're aware of 2 such architectures that also have a official rust target:
115+
// - WebAssembly: This architecture explicitly allows casting functions to ints and back.
116+
// - AVR: The only official target with this architecture (avr-none) has a pointer width of 16.
117+
// Which we don't support.
118+
//
119+
// Additionally the rust `alloc` library uses the same trick for its allocation error hook and we require
120+
// `alloc` support in this crate.
121+
//
122+
// AtomicFnPtr when?
123+
124+
// See `get_error_handler` for why we need `Ordering::Release`.
125+
GLOBAL_ERROR_HANDLER.store(hook as *mut (), Ordering::Release);
126+
}
127+
}
128+
129+
#[cfg(not(feature = "configurable_error_handler"))]
130+
mod inner {
131+
/// Gets the global error handler. This is currently [`panic()`].
132+
pub fn get_error_handler() -> super::BevyErrorHandler {
133+
super::panic
134+
}
125135
}
126136

137+
pub use inner::*;
138+
127139
macro_rules! inner {
128140
($call:path, $e:ident, $c:ident) => {
129141
$call!(
@@ -181,3 +193,42 @@ pub fn trace(error: BevyError, ctx: ErrorContext) {
181193
#[track_caller]
182194
#[inline]
183195
pub fn ignore(_: BevyError, _: ErrorContext) {}
196+
197+
#[cfg(test)]
198+
mod tests {
199+
#![allow(
200+
clippy::allow_attributes,
201+
reason = "We can't use except because the allow attribute becomes redundant in some cases."
202+
)]
203+
204+
#[allow(
205+
unused,
206+
reason = "With the correct combination of features we might end up not compiling any tests."
207+
)]
208+
use super::*;
209+
210+
#[test]
211+
// This test only makes sense under miri
212+
#[cfg(miri)]
213+
fn default_handler() {
214+
// Check under miri that we aren't casting a null into a function pointer in the default case
215+
216+
// Don't trigger dead code elimination
217+
core::hint::black_box(get_error_handler());
218+
}
219+
220+
#[test]
221+
#[cfg(feature = "configurable_error_handler")]
222+
fn set_handler() {
223+
// We need to cast the function into a pointer ahead of time. The function pointers were randomly different otherwise.
224+
let new_handler = dont_handler_error as fn(_, _);
225+
226+
set_error_handler(new_handler);
227+
let handler = get_error_handler();
228+
229+
assert_eq!(handler as *const (), new_handler as *const ());
230+
}
231+
232+
#[cfg(feature = "configurable_error_handler")]
233+
fn dont_handler_error(_: BevyError, _: ErrorContext) {}
234+
}

crates/bevy_ecs/src/error/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! [`panic`] error handler function is used, resulting in a panic with the error message attached.
99
//!
1010
//! You can change the default behavior by registering a custom error handler.
11-
//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app.
11+
//! Use the [`set_error_handler`] method to set a custom error handler function for your entire app.
1212
//! In practice, this is generally feature-flagged: panicking or loudly logging errors in development,
1313
//! and quietly logging or ignoring them in production to avoid crashing the app.
1414
//!
@@ -36,7 +36,7 @@
3636
//! Remember to turn on the `configurable_error_handler` feature to set a global error handler!
3737
//!
3838
//! ```rust, ignore
39-
//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext};
39+
//! use bevy_ecs::error::{set_error_handler, BevyError, ErrorContext};
4040
//! use log::trace;
4141
//!
4242
//! fn my_error_handler(error: BevyError, ctx: ErrorContext) {
@@ -49,7 +49,7 @@
4949
//!
5050
//! fn main() {
5151
//! // This requires the "configurable_error_handler" feature to be enabled to be in scope.
52-
//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once.");
52+
//! set_error_handler(my_error_handler);
5353
//!
5454
//! // Initialize your Bevy App here
5555
//! }

crates/bevy_ecs/src/observer/runner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use core::any::Any;
33

44
use crate::{
55
component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType},
6-
error::{default_error_handler, ErrorContext},
6+
error::{get_error_handler, ErrorContext},
77
observer::{ObserverDescriptor, ObserverTrigger},
88
prelude::*,
99
query::DebugCheckedUnwrap,
@@ -458,7 +458,7 @@ fn hook_on_add<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
458458
..Default::default()
459459
};
460460

461-
let error_handler = default_error_handler();
461+
let error_handler = get_error_handler();
462462

463463
// Initialize System
464464
let system: *mut dyn ObserverSystem<E, B> =

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use tracing::{info_span, Span};
1414

1515
use crate::{
1616
archetype::ArchetypeComponentId,
17-
error::{default_error_handler, BevyError, ErrorContext, Result},
17+
error::{get_error_handler, BevyError, ErrorContext, Result},
1818
prelude::Resource,
1919
query::Access,
2020
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
@@ -538,7 +538,7 @@ impl ExecutorState {
538538
world: UnsafeWorldCell,
539539
) -> bool {
540540
let mut should_run = !self.skipped_systems.contains(system_index);
541-
let error_handler = default_error_handler();
541+
let error_handler = get_error_handler();
542542

543543
for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() {
544544
if self.evaluated_sets.contains(set_idx) {
@@ -787,7 +787,7 @@ unsafe fn evaluate_and_fold_conditions(
787787
conditions: &mut [BoxedCondition],
788788
world: UnsafeWorldCell,
789789
) -> bool {
790-
let error_handler = default_error_handler();
790+
let error_handler = get_error_handler();
791791

792792
#[expect(
793793
clippy::unnecessary_fold,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::info_span;
88
use std::eprintln;
99

1010
use crate::{
11-
error::{default_error_handler, BevyError, ErrorContext},
11+
error::{get_error_handler, BevyError, ErrorContext},
1212
schedule::{
1313
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
1414
},
@@ -167,7 +167,7 @@ impl SimpleExecutor {
167167
}
168168

169169
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool {
170-
let error_handler = default_error_handler();
170+
let error_handler = get_error_handler();
171171

172172
#[expect(
173173
clippy::unnecessary_fold,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use tracing::info_span;
88
use std::eprintln;
99

1010
use crate::{
11-
error::{default_error_handler, BevyError, ErrorContext},
11+
error::{get_error_handler, BevyError, ErrorContext},
1212
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
1313
world::World,
1414
};
@@ -211,7 +211,7 @@ impl SingleThreadedExecutor {
211211
}
212212

213213
fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool {
214-
let error_handler: fn(BevyError, ErrorContext) = default_error_handler();
214+
let error_handler: fn(BevyError, ErrorContext) = get_error_handler();
215215

216216
#[expect(
217217
clippy::unnecessary_fold,

crates/bevy_ecs/src/schedule/schedule.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use tracing::info_span;
2727

2828
use crate::{
2929
component::{ComponentId, Components, Tick},
30-
error::default_error_handler,
30+
error::get_error_handler,
3131
prelude::Component,
3232
resource::Resource,
3333
schedule::*,
@@ -441,7 +441,7 @@ impl Schedule {
441441
self.initialize(world)
442442
.unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label));
443443

444-
let error_handler = default_error_handler();
444+
let error_handler = get_error_handler();
445445

446446
#[cfg(not(feature = "bevy_debug_stepping"))]
447447
self.executor

crates/bevy_ecs/src/system/commands/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ use crate::{
8989
/// A [`Command`] can return a [`Result`](crate::error::Result),
9090
/// which will be passed to an [error handler](crate::error) if the `Result` is an error.
9191
///
92-
/// The [default error handler](crate::error::default_error_handler) panics.
93-
/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`.
92+
/// The [default error handler](crate::error::get_error_handler) panics.
93+
/// It can be configured by using the [`set_error_handler`](crate::error::set_error_handler) method.
9494
///
9595
/// Alternatively, you can customize the error handler for a specific command
9696
/// by calling [`Commands::queue_handled`].
@@ -509,7 +509,7 @@ impl<'w, 's> Commands<'w, 's> {
509509
/// Pushes a generic [`Command`] to the command queue.
510510
///
511511
/// If the [`Command`] returns a [`Result`],
512-
/// it will be handled using the [default error handler](crate::error::default_error_handler).
512+
/// it will be handled using the [default error handler](crate::error::get_error_handler).
513513
///
514514
/// To use a custom error handler, see [`Commands::queue_handled`].
515515
///
@@ -695,7 +695,7 @@ impl<'w, 's> Commands<'w, 's> {
695695
/// This command will fail if any of the given entities do not exist.
696696
///
697697
/// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError),
698-
/// which will be handled by the [default error handler](crate::error::default_error_handler).
698+
/// which will be handled by the [default error handler](crate::error::get_error_handler).
699699
#[track_caller]
700700
pub fn insert_batch<I, B>(&mut self, batch: I)
701701
where
@@ -726,7 +726,7 @@ impl<'w, 's> Commands<'w, 's> {
726726
/// This command will fail if any of the given entities do not exist.
727727
///
728728
/// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError),
729-
/// which will be handled by the [default error handler](crate::error::default_error_handler).
729+
/// which will be handled by the [default error handler](crate::error::get_error_handler).
730730
#[track_caller]
731731
pub fn insert_batch_if_new<I, B>(&mut self, batch: I)
732732
where
@@ -1223,8 +1223,8 @@ impl<'w, 's> Commands<'w, 's> {
12231223
/// An [`EntityCommand`] can return a [`Result`](crate::error::Result),
12241224
/// which will be passed to an [error handler](crate::error) if the `Result` is an error.
12251225
///
1226-
/// The [default error handler](crate::error::default_error_handler) panics.
1227-
/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`.
1226+
/// The [default error handler](crate::error::get_error_handler) panics.
1227+
/// It can be configured by using the [`set_error_handler`](crate::error::set_error_handler) method.
12281228
///
12291229
/// Alternatively, you can customize the error handler for a specific command
12301230
/// by calling [`EntityCommands::queue_handled`].
@@ -1767,7 +1767,7 @@ impl<'a> EntityCommands<'a> {
17671767
/// Pushes an [`EntityCommand`] to the queue,
17681768
/// which will get executed for the current [`Entity`].
17691769
///
1770-
/// The [default error handler](crate::error::default_error_handler)
1770+
/// The [default error handler](crate::error::get_error_handler)
17711771
/// will be used to handle error cases.
17721772
/// Every [`EntityCommand`] checks whether the entity exists at the time of execution
17731773
/// and returns an error if it does not.

examples/ecs/error_handling.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! enabled. This feature is disabled by default, as it may introduce runtime overhead, especially for commands.
66
77
use bevy::ecs::{
8-
error::{warn, GLOBAL_ERROR_HANDLER},
8+
error::{set_error_handler, warn},
99
world::DeferredWorld,
1010
};
1111
use bevy::math::sampling::UniformMeshSampler;
@@ -22,9 +22,7 @@ fn main() {
2222
// Here we set the global error handler using one of the built-in
2323
// error handlers. Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
2424
// `debug`, `trace` and `ignore`.
25-
GLOBAL_ERROR_HANDLER
26-
.set(warn)
27-
.expect("The error handler can only be set once, globally.");
25+
set_error_handler(warn);
2826

2927
let mut app = App::new();
3028

0 commit comments

Comments
 (0)