bevy_ecs/error/bevy_error.rs
1use alloc::boxed::Box;
2use core::{
3 error::Error,
4 fmt::{Debug, Display},
5};
6
7/// The built in "universal" Bevy error type. This has a blanket [`From`] impl for any type that implements Rust's [`Error`],
8/// meaning it can be used as a "catch all" error.
9///
10/// # Severity
11///
12/// Each [`BevyError`] carries a [`Severity`] value that indicates how serious the error is.
13/// While the levels within [`Severity`] correspond to traditional logging levels,
14/// these levels are fundamentally advisory metadata.
15/// The fallback error handler ultimately has discretion to respond to each of these errors
16/// according to its configuration.
17/// The error handler ultimately has discretion to respond to each of these errors according to its configuration.
18/// You can change the behavior of the fallback handler by modifying the [`FallbackErrorHandler`] resource.
19///
20/// By default, errors without an assigned severity use [`Severity::Panic`], and will cause your application to panic.
21/// You can change the severity of an error by using [`with_severity`], or [`map_severity`] on any [`Result`] type.
22///
23/// [`FallbackErrorHandler`]: crate::error::handler::FallbackErrorHandler
24/// [`with_severity`]: ResultSeverityExt::with_severity
25/// [`map_severity`]: ResultSeverityExt::map_severity
26///
27/// # Backtraces
28///
29/// When used with the `backtrace` Cargo feature, it can capture a backtrace when the error is constructed (generally in the [`From`] impl).
30///
31/// To enable backtrace capture on supported platforms,
32/// set the `RUST_BACKTRACE` environment variable.
33/// See [`Backtrace::capture`] for details.
34///
35/// When the error is printed, the backtrace will be displayed.
36/// By default, the backtrace will be trimmed down to filter out noise.
37/// To see the full backtrace, set the `BEVY_BACKTRACE=full` environment variable.
38///
39/// [`Backtrace::capture`]: https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html#method.capture
40///
41/// # Usage
42///
43/// ```
44/// # use bevy_ecs::prelude::*;
45///
46/// fn fallible_system() -> Result<(), BevyError> {
47/// // This will result in Rust's built-in ParseIntError, which will automatically
48/// // be converted into a BevyError.
49/// let parsed: usize = "I am not a number".parse()?;
50/// Ok(())
51/// }
52/// ```
53pub struct BevyError {
54 inner: Box<InnerBevyError>,
55}
56
57impl BevyError {
58 /// Constructs a new [`BevyError`] with the given [`Severity`].
59 ///
60 /// The error will be stored as a `Box<dyn Error + Send + Sync>`.
61 ///
62 /// The easiest way to use this is to pass in a string.
63 /// This works because any type that can be converted into a `Box<dyn Error + Send + Sync>` can be used,
64 /// and [`str`] is one such type.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// # use bevy_ecs::error::{BevyError, Severity};
70 ///
71 /// fn some_function(val: i64) -> Result<(), BevyError> {
72 /// if val < 0 {
73 /// let error =
74 /// BevyError::new(Severity::Panic, format!("Value can't be negative {val}"));
75 /// return Err(error);
76 /// }
77 ///
78 /// // ...
79 /// Ok(())
80 /// }
81 /// ```
82 pub fn new<E>(severity: Severity, error: E) -> Self
83 where
84 Box<dyn Error + Sync + Send>: From<E>,
85 {
86 Self::from(error).with_severity(severity)
87 }
88
89 /// Creates a new [`BevyError`] with the [`Severity::Ignore`] severity.
90 ///
91 /// This is a shorthand for <code>[BevyError::new(Severity::Ignore, error)](BevyError::new)</code>.
92 pub fn ignore<E>(error: E) -> Self
93 where
94 Box<dyn Error + Send + Sync>: From<E>,
95 {
96 Self::new(Severity::Ignore, error)
97 }
98
99 /// Creates a new [`BevyError`] with the [`Severity::Trace`] severity.
100 ///
101 /// This is a shorthand for <code>[BevyError::new(Severity::Trace, error)](BevyError::new)</code>.
102 pub fn trace<E>(error: E) -> Self
103 where
104 Box<dyn Error + Send + Sync>: From<E>,
105 {
106 Self::new(Severity::Trace, error)
107 }
108
109 /// Creates a new [`BevyError`] with the [`Severity::Debug`] severity.
110 ///
111 /// This is a shorthand for <code>[BevyError::new(Severity::Debug, error)](BevyError::new)</code>.
112 pub fn debug<E>(error: E) -> Self
113 where
114 Box<dyn Error + Send + Sync>: From<E>,
115 {
116 Self::new(Severity::Debug, error)
117 }
118
119 /// Creates a new [`BevyError`] with the [`Severity::Info`] severity.
120 ///
121 /// This is a shorthand for <code>[BevyError::new(Severity::Info, error)](BevyError::new)</code>.
122 pub fn info<E>(error: E) -> Self
123 where
124 Box<dyn Error + Send + Sync>: From<E>,
125 {
126 Self::new(Severity::Info, error)
127 }
128
129 /// Creates a new [`BevyError`] with the [`Severity::Warning`] severity.
130 ///
131 /// This is a shorthand for <code>[BevyError::new(Severity::Warning, error)](BevyError::new)</code>.
132 pub fn warning<E>(error: E) -> Self
133 where
134 Box<dyn Error + Send + Sync>: From<E>,
135 {
136 Self::new(Severity::Warning, error)
137 }
138
139 /// Creates a new [`BevyError`] with the [`Severity::Error`] severity.
140 ///
141 /// This is a shorthand for <code>[BevyError::new(Severity::Error, error)](BevyError::new)</code>.
142 pub fn error<E>(error: E) -> Self
143 where
144 Box<dyn Error + Send + Sync>: From<E>,
145 {
146 Self::new(Severity::Error, error)
147 }
148
149 /// Creates a new [`BevyError`] with the [`Severity::Panic`] severity.
150 ///
151 /// This is a shorthand for <code>[BevyError::new(Severity::Panic, error)](BevyError::new)</code>.
152 pub fn panic<E>(error: E) -> Self
153 where
154 Box<dyn Error + Send + Sync>: From<E>,
155 {
156 Self::new(Severity::Panic, error)
157 }
158
159 /// Checks if the internal error is of the given type.
160 pub fn is<E: Error + 'static>(&self) -> bool {
161 self.inner.error.is::<E>()
162 }
163
164 /// Attempts to downcast the internal error to the given type.
165 pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {
166 self.inner.error.downcast_ref::<E>()
167 }
168
169 fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
170 #[cfg(feature = "backtrace")]
171 {
172 let f = _f;
173 let backtrace = &self.inner.backtrace;
174 if let std::backtrace::BacktraceStatus::Captured = backtrace.status() {
175 // TODO: Cache
176 let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full");
177
178 let backtrace_str = alloc::string::ToString::to_string(backtrace);
179 let mut skip_next_location_line = false;
180 for line in backtrace_str.split('\n') {
181 if !full_backtrace {
182 if skip_next_location_line {
183 if line.starts_with(" at") {
184 continue;
185 }
186 skip_next_location_line = false;
187 }
188 if line.contains("std::backtrace_rs::backtrace::") {
189 skip_next_location_line = true;
190 continue;
191 }
192 if line.contains("std::backtrace::Backtrace::") {
193 skip_next_location_line = true;
194 continue;
195 }
196 if line.contains("<bevy_ecs::error::bevy_error::BevyError as core::convert::From<E>>::from") {
197 skip_next_location_line = true;
198 continue;
199 }
200 if line.contains("<core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual") {
201 skip_next_location_line = true;
202 continue;
203 }
204 if line.contains("__rust_begin_short_backtrace") {
205 break;
206 }
207 if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") {
208 break;
209 }
210 }
211 writeln!(f, "{line}")?;
212 }
213 if !full_backtrace {
214 if std::thread::panicking() {
215 SKIP_NORMAL_BACKTRACE.set(true);
216 }
217 writeln!(f, "{FILTER_MESSAGE}")?;
218 }
219 }
220 }
221 Ok(())
222 }
223}
224
225/// This type exists (rather than having a `BevyError(Box<dyn InnerBevyError)`) to make [`BevyError`] use a "thin pointer" instead of
226/// a "fat pointer", which reduces the size of our Result by a usize. This does introduce an extra indirection, but error handling is a "cold path".
227/// We don't need to optimize it to that degree.
228/// PERF: We could probably have the best of both worlds with a "custom vtable" impl, but that's not a huge priority right now and the code simplicity
229/// of the current impl is nice.
230struct InnerBevyError {
231 error: Box<dyn Error + Send + Sync + 'static>,
232 severity: Severity,
233 #[cfg(feature = "backtrace")]
234 backtrace: std::backtrace::Backtrace,
235}
236
237/// Indicates how severe a [`BevyError`] is.
238///
239/// These levels correspond to traditional logging levels,
240/// but the severity is advisory metadata used by error handlers to decide how to react (for example: ignore, log, or panic).
241///
242/// To change the behavior of unhandled errors returned from systems,
243/// you can modify the [fallback error handler], and read the [`Severity`] stored inside of each [`BevyError`].
244///
245/// You can change the severity of an error (including assigning an error severity) to an ordinary result
246/// by calling [`with_severity`] or [`map_severity`].
247///
248/// [`with_severity`]: ResultSeverityExt::with_severity
249/// [`map_severity`]: ResultSeverityExt::map_severity
250/// [fallback error handler]: crate::error::handler::FallbackErrorHandler
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
252pub enum Severity {
253 /// The error can be safely ignored, and can be completely discarded.
254 Ignore,
255 /// The error can be ignored, unless verbose debugging is required.
256 Trace,
257 /// The error can be safely ignored, but may need to be surfaced during debugging.
258 Debug,
259 /// Nothing has gone wrong, but the error is useful to the user and should be reported.
260 Info,
261 /// Something unexpected but recoverable happened.
262 ///
263 /// Something has probably gone wrong.
264 Warning,
265 /// A real error occurred, but the program may continue.
266 Error,
267 /// A fatal error; the program cannot continue.
268 Panic,
269}
270
271impl BevyError {
272 /// Returns the severity of this error.
273 pub fn severity(&self) -> Severity {
274 self.inner.severity
275 }
276
277 /// Returns this error with its severity overridden.
278 ///
279 /// Note that this doesn't change the underlying error value;
280 /// only the [`Severity`] metadata used by the error handler.
281 pub fn with_severity(mut self, severity: Severity) -> Self {
282 self.inner.severity = severity;
283 self
284 }
285}
286
287/// Extension methods for annotating errors with a [`Severity`].
288pub trait ResultSeverityExt<T, E>: Sized {
289 /// Overrides the [`Severity`] of the error if this result is `Err`.
290 /// This does not change control flow; it only annotates the error.
291 ///
292 /// # Example
293 /// ```
294 /// # use bevy_ecs::error::{BevyError, ResultSeverityExt, Severity};
295 /// fn fallible() -> Result<(), BevyError> {
296 /// // This failure is expected in some contexts, so we downgrade its severity.
297 /// let _parsed: usize = "I am not a number"
298 /// .parse()
299 /// .with_severity(Severity::Warning)?;
300 /// Ok(())
301 /// }
302 /// ```
303 ///
304 /// For more fine grained control see [`Result::map_severity`]
305 fn with_severity(self, severity: Severity) -> Result<T, BevyError>;
306
307 /// Overrides the [`Severity`] of the error if this result is `Err`.
308 /// This does not change control flow; it only annotates the error.
309 ///
310 /// # Example
311 /// ```
312 /// # use bevy_ecs::error::{BevyError, ResultSeverityExt, Severity};
313 /// # use thiserror::Error;
314 /// # fn validate(_string: &str) -> Result<usize, ValidationError> {
315 /// # Err(ValidationError::IncorrectVersion)
316 /// # }
317 ///
318 /// #[derive(Error, Debug)]
319 /// pub enum ValidationError {
320 /// #[error("Incorrect version")]
321 /// IncorrectVersion,
322 /// #[error("Syntax error")]
323 /// SyntaxError,
324 /// }
325 ///
326 /// fn fallible() -> Result<(), BevyError> {
327 /// // This failure is expected in some contexts, so we downgrade its severity.
328 /// let _parsed: usize = validate("I am not a number")
329 /// .map_severity(|e| match e {
330 /// ValidationError::IncorrectVersion => Severity::Debug,
331 /// ValidationError::SyntaxError => Severity::Error,
332 /// })?;
333 /// Ok(())
334 /// }
335 /// ```
336 ///
337 /// If you don't need to inspect the error, use [`Result::with_severity`]
338 fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError>;
339
340 /// Overrides the severity of the error with [`Severity::Ignore`]. See [`Result::with_severity`]
341 ///
342 /// This is shorthand for `self.with_severity(Severity::Ignore)`
343 fn ignore(self) -> Result<T, BevyError> {
344 self.with_severity(Severity::Ignore)
345 }
346
347 /// Overrides the severity of the error with [`Severity::Trace`]. See [`Result::with_severity`]
348 ///
349 /// This is shorthand for `self.with_severity(Severity::Trace)`
350 fn trace(self) -> Result<T, BevyError> {
351 self.with_severity(Severity::Trace)
352 }
353
354 /// Overrides the severity of the error with [`Severity::Info`]. See [`Result::with_severity`]
355 ///
356 /// This is shorthand for `self.with_severity(Severity::Info)`
357 fn info(self) -> Result<T, BevyError> {
358 self.with_severity(Severity::Info)
359 }
360
361 /// Overrides the severity of the error with [`Severity::Warning`]. See [`Result::with_severity`]
362 ///
363 /// This is shorthand for `self.with_severity(Severity::Warning)`
364 fn warn(self) -> Result<T, BevyError> {
365 self.with_severity(Severity::Warning)
366 }
367
368 /// Overrides the severity of the error with [`Severity::Error`]. See [`Result::with_severity`]
369 ///
370 /// This is shorthand for `self.with_severity(Severity::Error)`
371 fn error(self) -> Result<T, BevyError> {
372 self.with_severity(Severity::Error)
373 }
374
375 /// Overrides the severity of the error with [`Severity::Panic`]. See [`Result::with_severity`]
376 ///
377 /// This is shorthand for `self.with_severity(Severity::Panic)`
378 fn panic(self) -> Result<T, BevyError> {
379 self.with_severity(Severity::Panic)
380 }
381}
382
383impl<T, E> ResultSeverityExt<T, E> for Result<T, E>
384where
385 E: Into<BevyError>,
386{
387 fn with_severity(self, severity: Severity) -> Result<T, BevyError> {
388 self.map_err(|e| e.into().with_severity(severity))
389 }
390
391 fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError> {
392 self.map_err(|e| {
393 let severity = f(&e);
394 e.into().with_severity(severity)
395 })
396 }
397}
398
399// NOTE: writing the impl this way gives us From<&str> ... nice!
400impl<E> From<E> for BevyError
401where
402 Box<dyn Error + Send + Sync + 'static>: From<E>,
403{
404 #[cold]
405 fn from(error: E) -> Self {
406 BevyError {
407 inner: Box::new(InnerBevyError {
408 error: error.into(),
409 severity: Severity::Panic,
410 #[cfg(feature = "backtrace")]
411 backtrace: std::backtrace::Backtrace::capture(),
412 }),
413 }
414 }
415}
416
417impl Display for BevyError {
418 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
419 writeln!(f, "{}", self.inner.error)?;
420 self.format_backtrace(f)?;
421 Ok(())
422 }
423}
424
425impl Debug for BevyError {
426 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
427 writeln!(f, "{:?}", self.inner.error)?;
428 self.format_backtrace(f)?;
429 Ok(())
430 }
431}
432
433#[cfg(feature = "backtrace")]
434const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
435
436#[cfg(feature = "backtrace")]
437std::thread_local! {
438 static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
439 const { core::cell::Cell::new(false) };
440}
441
442/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.
443#[cfg(feature = "backtrace")]
444#[expect(clippy::print_stdout, reason = "Allowed behind `std` feature gate.")]
445pub fn bevy_error_panic_hook(
446 current_hook: impl Fn(&std::panic::PanicHookInfo),
447) -> impl Fn(&std::panic::PanicHookInfo) {
448 move |info| {
449 if SKIP_NORMAL_BACKTRACE.replace(false) {
450 if let Some(payload) = info.payload().downcast_ref::<&str>() {
451 std::println!("{payload}");
452 } else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {
453 std::println!("{payload}");
454 }
455 return;
456 }
457
458 current_hook(info);
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use crate::error::BevyError;
465
466 #[test]
467 #[cfg(not(miri))] // miri backtraces are weird
468 #[cfg(not(windows))] // the windows backtrace in this context is ... unhelpful and not worth testing
469 fn filtered_backtrace_test() {
470 fn i_fail() -> crate::error::Result {
471 let _: usize = "I am not a number".parse()?;
472 Ok(())
473 }
474
475 let capture_backtrace = std::env::var_os("RUST_BACKTRACE");
476
477 if capture_backtrace.is_none() || capture_backtrace.clone().is_some_and(|s| s == "0") {
478 panic!("This test only works if rust backtraces are enabled. Value set was {capture_backtrace:?}. Please set RUST_BACKTRACE to any value other than 0 and run again.")
479 }
480
481 let error = i_fail().err().unwrap();
482 let debug_message = alloc::format!("{error:?}");
483 let mut lines = debug_message.lines().peekable();
484 assert_eq!(
485 "ParseIntError { kind: InvalidDigit }",
486 lines.next().unwrap()
487 );
488
489 // On mac backtraces can start with Backtrace::create
490 // Rust 1.95 changed the format to use angle brackets: <std::backtrace::Backtrace>::create
491 let mut skip = false;
492 if let Some(line) = lines.peek()
493 && (line[6..] == *"std::backtrace::Backtrace::create"
494 || line[6..] == *"<std::backtrace::Backtrace>::create")
495 {
496 skip = true;
497 }
498
499 if skip {
500 lines.next().unwrap();
501 }
502
503 let expected_lines = alloc::vec![
504 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail",
505 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test",
506 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{{closure}}",
507 "core::ops::function::FnOnce::call_once",
508 ];
509
510 for expected in expected_lines {
511 let line = lines.next().unwrap();
512 assert_eq!(&line[6..], expected);
513 let mut skip = false;
514 if let Some(line) = lines.peek()
515 && line.starts_with(" at")
516 {
517 skip = true;
518 }
519
520 if skip {
521 lines.next().unwrap();
522 }
523 }
524
525 // on linux there is a second call_once
526 let mut skip = false;
527 if let Some(line) = lines.peek()
528 && &line[6..] == "core::ops::function::FnOnce::call_once"
529 {
530 skip = true;
531 }
532
533 if skip {
534 lines.next().unwrap();
535 }
536 let mut skip = false;
537 if let Some(line) = lines.peek()
538 && line.starts_with(" at")
539 {
540 skip = true;
541 }
542
543 if skip {
544 lines.next().unwrap();
545 }
546 assert_eq!(super::FILTER_MESSAGE, lines.next().unwrap());
547 assert!(lines.next().is_none());
548 }
549
550 #[test]
551 fn downcasting() {
552 #[derive(Debug, PartialEq)]
553 struct Fun(i32);
554
555 impl core::fmt::Display for Fun {
556 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
557 core::fmt::Debug::fmt(&self, f)
558 }
559 }
560 impl core::error::Error for Fun {}
561
562 let new_error = BevyError::new(crate::error::Severity::Debug, Fun(1));
563
564 assert!(new_error.is::<Fun>());
565 assert_eq!(new_error.downcast_ref::<Fun>(), Some(&Fun(1)));
566 }
567}