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/// # Backtraces
11///
12/// When used with the `backtrace` Cargo feature, it will capture a backtrace when the error is constructed (generally in the [`From`] impl]).
13/// When printed, the backtrace will be displayed. By default, the backtrace will be trimmed down to filter out noise. To see the full backtrace,
14/// set the `BEVY_BACKTRACE=full` environment variable.
15///
16/// # Usage
17///
18/// ```
19/// # use bevy_ecs::prelude::*;
20///
21/// fn fallible_system() -> Result<(), BevyError> {
22///     // This will result in Rust's built-in ParseIntError, which will automatically
23///     // be converted into a BevyError.
24///     let parsed: usize = "I am not a number".parse()?;
25///     Ok(())
26/// }
27/// ```
28pub struct BevyError {
29    inner: Box<InnerBevyError>,
30}
31
32impl BevyError {
33    /// Attempts to downcast the internal error to the given type.
34    pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {
35        self.inner.error.downcast_ref::<E>()
36    }
37
38    fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39        #[cfg(feature = "backtrace")]
40        {
41            let f = _f;
42            let backtrace = &self.inner.backtrace;
43            if let std::backtrace::BacktraceStatus::Captured = backtrace.status() {
44                let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full");
45
46                let backtrace_str = alloc::string::ToString::to_string(backtrace);
47                let mut skip_next_location_line = false;
48                for line in backtrace_str.split('\n') {
49                    if !full_backtrace {
50                        if skip_next_location_line {
51                            if line.starts_with("             at") {
52                                continue;
53                            }
54                            skip_next_location_line = false;
55                        }
56                        if line.contains("std::backtrace_rs::backtrace::") {
57                            skip_next_location_line = true;
58                            continue;
59                        }
60                        if line.contains("std::backtrace::Backtrace::") {
61                            skip_next_location_line = true;
62                            continue;
63                        }
64                        if line.contains("<bevy_ecs::error::bevy_error::BevyError as core::convert::From<E>>::from") {
65                            skip_next_location_line = true;
66                            continue;
67                        }
68                        if line.contains("<core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual") {
69                            skip_next_location_line = true;
70                            continue;
71                        }
72                        if line.contains("__rust_begin_short_backtrace") {
73                            break;
74                        }
75                        if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") {
76                            break;
77                        }
78                    }
79                    writeln!(f, "{}", line)?;
80                }
81                if !full_backtrace {
82                    if std::thread::panicking() {
83                        SKIP_NORMAL_BACKTRACE.set(true);
84                    }
85                    writeln!(f, "{FILTER_MESSAGE}")?;
86                }
87            }
88        }
89        Ok(())
90    }
91}
92
93/// This type exists (rather than having a `BevyError(Box<dyn InnerBevyError)`) to make [`BevyError`] use a "thin pointer" instead of
94/// 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".
95/// We don't need to optimize it to that degree.
96/// PERF: We could probably have the best of both worlds with a "custom vtable" impl, but thats not a huge priority right now and the code simplicity
97/// of the current impl is nice.
98struct InnerBevyError {
99    error: Box<dyn Error + Send + Sync + 'static>,
100    #[cfg(feature = "backtrace")]
101    backtrace: std::backtrace::Backtrace,
102}
103
104// NOTE: writing the impl this way gives us From<&str> ... nice!
105impl<E> From<E> for BevyError
106where
107    Box<dyn Error + Send + Sync + 'static>: From<E>,
108{
109    #[cold]
110    fn from(error: E) -> Self {
111        BevyError {
112            inner: Box::new(InnerBevyError {
113                error: error.into(),
114                #[cfg(feature = "backtrace")]
115                backtrace: std::backtrace::Backtrace::capture(),
116            }),
117        }
118    }
119}
120
121impl Display for BevyError {
122    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123        writeln!(f, "{}", self.inner.error)?;
124        self.format_backtrace(f)?;
125        Ok(())
126    }
127}
128
129impl Debug for BevyError {
130    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
131        writeln!(f, "{:?}", self.inner.error)?;
132        self.format_backtrace(f)?;
133        Ok(())
134    }
135}
136
137#[cfg(feature = "backtrace")]
138const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
139
140#[cfg(feature = "backtrace")]
141std::thread_local! {
142    static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
143        const { core::cell::Cell::new(false) };
144}
145
146/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.
147#[cfg(feature = "backtrace")]
148#[expect(clippy::print_stdout, reason = "Allowed behind `std` feature gate.")]
149pub fn bevy_error_panic_hook(
150    current_hook: impl Fn(&std::panic::PanicHookInfo),
151) -> impl Fn(&std::panic::PanicHookInfo) {
152    move |info| {
153        if SKIP_NORMAL_BACKTRACE.replace(false) {
154            if let Some(payload) = info.payload().downcast_ref::<&str>() {
155                std::println!("{payload}");
156            } else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {
157                std::println!("{payload}");
158            }
159            return;
160        }
161
162        current_hook(info);
163    }
164}
165
166#[cfg(test)]
167mod tests {
168
169    #[test]
170    #[cfg(not(miri))] // miri backtraces are weird
171    #[cfg(not(windows))] // the windows backtrace in this context is ... unhelpful and not worth testing
172    fn filtered_backtrace_test() {
173        fn i_fail() -> crate::error::Result {
174            let _: usize = "I am not a number".parse()?;
175            Ok(())
176        }
177
178        // SAFETY: this is not safe ...  this test could run in parallel with another test
179        // that writes the environment variable. We either accept that so we can write this test,
180        // or we don't.
181
182        unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
183
184        let error = i_fail().err().unwrap();
185        let debug_message = alloc::format!("{error:?}");
186        let mut lines = debug_message.lines().peekable();
187        assert_eq!(
188            "ParseIntError { kind: InvalidDigit }",
189            lines.next().unwrap()
190        );
191
192        // On mac backtraces can start with Backtrace::create
193        let mut skip = false;
194        if let Some(line) = lines.peek() {
195            if &line[6..] == "std::backtrace::Backtrace::create" {
196                skip = true;
197            }
198        }
199
200        if skip {
201            lines.next().unwrap();
202        }
203
204        let expected_lines = alloc::vec![
205            "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail",
206            "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test",
207            "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{{closure}}",
208            "core::ops::function::FnOnce::call_once",
209        ];
210
211        for expected in expected_lines {
212            let line = lines.next().unwrap();
213            assert_eq!(&line[6..], expected);
214            let mut skip = false;
215            if let Some(line) = lines.peek() {
216                if line.starts_with("             at") {
217                    skip = true;
218                }
219            }
220
221            if skip {
222                lines.next().unwrap();
223            }
224        }
225
226        // on linux there is a second call_once
227        let mut skip = false;
228        if let Some(line) = lines.peek() {
229            if &line[6..] == "core::ops::function::FnOnce::call_once" {
230                skip = true;
231            }
232        }
233        if skip {
234            lines.next().unwrap();
235        }
236        let mut skip = false;
237        if let Some(line) = lines.peek() {
238            if line.starts_with("             at") {
239                skip = true;
240            }
241        }
242
243        if skip {
244            lines.next().unwrap();
245        }
246        assert_eq!(super::FILTER_MESSAGE, lines.next().unwrap());
247        assert!(lines.next().is_none());
248    }
249}