Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ecs/src/error/bevy_error.rs
6604 views
1
use alloc::boxed::Box;
2
use 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
/// ```
28
pub struct BevyError {
29
inner: Box<InnerBevyError>,
30
}
31
32
impl 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.
98
struct 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!
105
impl<E> From<E> for BevyError
106
where
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
121
impl 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
129
impl 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")]
138
const 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")]
141
std::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.")]
149
pub 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)]
167
mod 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
}
250
251