Skip to main content

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}