bevy_ecs/error/bevy_error.rs
1use alloc::{borrow::Cow, boxed::Box};
2use bevy_platform::cell::SyncCell;
3use core::{
4 any::Any,
5 error::Error,
6 fmt::{Debug, Display},
7};
8
9/// The built in "universal" Bevy error type. This has a blanket [`From`] impl for any type that implements Rust's [`Error`],
10/// meaning it can be used as a "catch all" error.
11///
12/// # Severity
13///
14/// Each [`BevyError`] carries a [`Severity`] value that indicates how serious the error is.
15/// While the levels within [`Severity`] correspond to traditional logging levels,
16/// these levels are fundamentally advisory metadata.
17/// The fallback error handler ultimately has discretion to respond to each of these errors
18/// according to its configuration.
19/// The error handler ultimately has discretion to respond to each of these errors according to its configuration.
20/// You can change the behavior of the fallback handler by modifying the [`FallbackErrorHandler`] resource.
21///
22/// By default, errors without an assigned severity use [`Severity::Panic`], and will cause your application to panic.
23/// You can change the severity of an error by using [`with_severity`], or [`map_severity`] on any [`Result`] type.
24///
25/// [`FallbackErrorHandler`]: crate::error::handler::FallbackErrorHandler
26/// [`with_severity`]: ResultSeverityExt::with_severity
27/// [`map_severity`]: ResultSeverityExt::map_severity
28///
29/// # Backtraces
30///
31/// When used with the `backtrace` Cargo feature, it can capture a backtrace when the error is constructed (generally in the [`From`] impl).
32///
33/// To enable backtrace capture on supported platforms,
34/// set the `RUST_BACKTRACE` environment variable.
35/// See [`Backtrace::capture`] for details.
36///
37/// When the error is printed, the backtrace will be displayed.
38/// By default, the backtrace will be trimmed down to filter out noise.
39/// To see the full backtrace, set the `BEVY_BACKTRACE=full` environment variable.
40///
41/// [`Backtrace::capture`]: https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html#method.capture
42///
43/// # Context
44///
45/// You can attach a context message to a [`Result`] or [`Option`] value to turn it into
46/// a [`Result`] with a [`BevyError`] using [`context`] or [`with_context`].
47/// The resulting error will have the message passed to [`context`] added to it.
48///
49/// [`context`]: ContextExt::context
50/// [`with_context`]: ContextExt::with_context
51///
52/// # Usage
53///
54/// ```
55/// # use bevy_ecs::prelude::*;
56///
57/// fn fallible_system() -> Result<(), BevyError> {
58/// // This will result in Rust's built-in ParseIntError, which will automatically
59/// // be converted into a BevyError with an additional message.
60/// let parsed: usize = "I am not a number".parse().context("failed to parse number")?;
61/// Ok(())
62/// }
63/// ```
64pub struct BevyError {
65 inner: Box<InnerBevyError>,
66}
67
68impl BevyError {
69 /// Constructs a new [`BevyError`] with the given [`Severity`].
70 ///
71 /// The error will be stored as a `Box<dyn Error + Send + Sync>`.
72 ///
73 /// The easiest way to use this is to pass in a string.
74 /// This works because any type that can be converted into a `Box<dyn Error + Send + Sync>` can be used,
75 /// and [`str`] is one such type.
76 ///
77 /// # Examples
78 ///
79 /// ```
80 /// # use bevy_ecs::error::{BevyError, Severity};
81 ///
82 /// fn some_function(val: i64) -> Result<(), BevyError> {
83 /// if val < 0 {
84 /// // Consider using the bevy_error! or even the bail! macro for format! support
85 /// let error =
86 /// BevyError::new(Severity::Panic, format!("Value can't be negative {val}"));
87 /// return Err(error);
88 /// }
89 ///
90 /// // ...
91 /// Ok(())
92 /// }
93 /// ```
94 pub fn new<E>(severity: Severity, error: E) -> Self
95 where
96 Box<dyn Error + Sync + Send>: From<E>,
97 {
98 Self::from(error).with_severity(severity)
99 }
100
101 /// Constructs a new [`BevyError`] with the given [`Severity`].
102 ///
103 /// Like [`BevyError::new`], but if the `backtrace` cargo feature is enabled
104 /// it will use the supplied backtrace instead of capturing a new one.
105 #[cfg(feature = "std")]
106 pub fn new_with_backtrace<E>(
107 severity: Severity,
108 error: E,
109 backtrace: std::backtrace::Backtrace,
110 ) -> Self
111 where
112 Box<dyn Error + Sync + Send>: From<E>,
113 {
114 #[cfg(not(feature = "backtrace"))]
115 drop(backtrace);
116 BevyError {
117 inner: Box::new(InnerBevyError {
118 error: error.into(),
119 severity,
120 context: alloc::vec![],
121 panic_payload: None,
122 #[cfg(feature = "backtrace")]
123 backtrace,
124 }),
125 }
126 }
127
128 /// Creates a new [`BevyError`] with the [`Severity::Ignore`] severity.
129 ///
130 /// This is a shorthand for <code>[BevyError::new(Severity::Ignore, error)](BevyError::new)</code>.
131 pub fn ignore<E>(error: E) -> Self
132 where
133 Box<dyn Error + Send + Sync>: From<E>,
134 {
135 Self::new(Severity::Ignore, error)
136 }
137
138 /// Creates a new [`BevyError`] with the [`Severity::Trace`] severity.
139 ///
140 /// This is a shorthand for <code>[BevyError::new(Severity::Trace, error)](BevyError::new)</code>.
141 pub fn trace<E>(error: E) -> Self
142 where
143 Box<dyn Error + Send + Sync>: From<E>,
144 {
145 Self::new(Severity::Trace, error)
146 }
147
148 /// Creates a new [`BevyError`] with the [`Severity::Debug`] severity.
149 ///
150 /// This is a shorthand for <code>[BevyError::new(Severity::Debug, error)](BevyError::new)</code>.
151 pub fn debug<E>(error: E) -> Self
152 where
153 Box<dyn Error + Send + Sync>: From<E>,
154 {
155 Self::new(Severity::Debug, error)
156 }
157
158 /// Creates a new [`BevyError`] with the [`Severity::Info`] severity.
159 ///
160 /// This is a shorthand for <code>[BevyError::new(Severity::Info, error)](BevyError::new)</code>.
161 pub fn info<E>(error: E) -> Self
162 where
163 Box<dyn Error + Send + Sync>: From<E>,
164 {
165 Self::new(Severity::Info, error)
166 }
167
168 /// Creates a new [`BevyError`] with the [`Severity::Warning`] severity.
169 ///
170 /// This is a shorthand for <code>[BevyError::new(Severity::Warning, error)](BevyError::new)</code>.
171 pub fn warning<E>(error: E) -> Self
172 where
173 Box<dyn Error + Send + Sync>: From<E>,
174 {
175 Self::new(Severity::Warning, error)
176 }
177
178 /// Creates a new [`BevyError`] with the [`Severity::Error`] severity.
179 ///
180 /// This is a shorthand for <code>[BevyError::new(Severity::Error, error)](BevyError::new)</code>.
181 pub fn error<E>(error: E) -> Self
182 where
183 Box<dyn Error + Send + Sync>: From<E>,
184 {
185 Self::new(Severity::Error, error)
186 }
187
188 /// Creates a new [`BevyError`] with the [`Severity::Panic`] severity.
189 ///
190 /// This is a shorthand for <code>[BevyError::new(Severity::Panic, error)](BevyError::new)</code>.
191 pub fn panic<E>(error: E, payload: Box<dyn Any + Send>) -> Self
192 where
193 Box<dyn Error + Send + Sync>: From<E>,
194 {
195 Self::new(Severity::Panic, error).with_payload(payload)
196 }
197
198 /// Checks if the internal error is of the given type.
199 pub fn is<E: Error + 'static>(&self) -> bool {
200 self.inner.error.is::<E>()
201 }
202
203 /// Attempts to downcast the internal error to the given type.
204 pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {
205 self.inner.error.downcast_ref::<E>()
206 }
207
208 fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209 #[cfg(feature = "backtrace")]
210 {
211 let f = _f;
212 let backtrace = &self.inner.backtrace;
213 if let std::backtrace::BacktraceStatus::Captured = backtrace.status() {
214 // TODO: Cache
215 let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full");
216
217 let backtrace_str = alloc::string::ToString::to_string(backtrace);
218 let mut skip_next_location_line = false;
219 for line in backtrace_str.split('\n') {
220 if !full_backtrace {
221 if skip_next_location_line {
222 if line.starts_with(" at") {
223 continue;
224 }
225 skip_next_location_line = false;
226 }
227 if line.contains("std::backtrace_rs::backtrace::") {
228 skip_next_location_line = true;
229 continue;
230 }
231 if line.contains(": std::backtrace::Backtrace::")
232 || line.contains(": <std::backtrace::Backtrace>::")
233 {
234 skip_next_location_line = true;
235 continue;
236 }
237 if line.contains("<bevy_ecs::error::bevy_error::BevyError as core::convert::From<E>>::from") {
238 skip_next_location_line = true;
239 continue;
240 }
241 if line.contains("<core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual") {
242 skip_next_location_line = true;
243 continue;
244 }
245 if line.contains("__rust_begin_short_backtrace") {
246 break;
247 }
248 if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") {
249 break;
250 }
251 }
252 writeln!(f, "{line}")?;
253 }
254 if !full_backtrace {
255 if std::thread::panicking() {
256 SKIP_NORMAL_BACKTRACE.set(true);
257 }
258 writeln!(f, "{FILTER_MESSAGE}")?;
259 }
260 }
261 }
262 Ok(())
263 }
264}
265
266/// This type exists (rather than having a `BevyError(Box<dyn InnerBevyError)`) to make [`BevyError`] use a "thin pointer" instead of
267/// 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".
268/// We don't need to optimize it to that degree.
269/// 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
270/// of the current impl is nice.
271struct InnerBevyError {
272 error: Box<dyn Error + Send + Sync + 'static>,
273 context: alloc::vec::Vec<Cow<'static, str>>,
274 severity: Severity,
275 // The panic payload from `catch_unwind` is a `Box<dyn Any + Send>`. We need `BevyError` to be `Sync` so we store that
276 // in a `SyncCell`. We store it in an `Option` because we ownership of the payload to do things with it.
277 panic_payload: Option<SyncCell<Box<dyn Any + Send>>>,
278 #[cfg(feature = "backtrace")]
279 backtrace: std::backtrace::Backtrace,
280}
281
282/// Indicates how severe a [`BevyError`] is.
283///
284/// These levels correspond to traditional logging levels,
285/// but the severity is advisory metadata used by error handlers to decide how to react (for example: ignore, log, or panic).
286///
287/// To change the behavior of unhandled errors returned from systems,
288/// you can modify the [fallback error handler], and read the [`Severity`] stored inside of each [`BevyError`].
289///
290/// You can change the severity of an error (including assigning an error severity) to an ordinary result
291/// by calling [`with_severity`] or [`map_severity`].
292///
293/// [`with_severity`]: ResultSeverityExt::with_severity
294/// [`map_severity`]: ResultSeverityExt::map_severity
295/// [fallback error handler]: crate::error::handler::FallbackErrorHandler
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
297pub enum Severity {
298 /// The error can be safely ignored, and can be completely discarded.
299 Ignore,
300 /// The error can be ignored, unless verbose debugging is required.
301 Trace,
302 /// The error can be safely ignored, but may need to be surfaced during debugging.
303 Debug,
304 /// Nothing has gone wrong, but the error is useful to the user and should be reported.
305 Info,
306 /// Something unexpected but recoverable happened.
307 ///
308 /// Something has probably gone wrong.
309 Warning,
310 /// A real error occurred, but the program may continue.
311 Error,
312 /// A fatal error; the program cannot continue.
313 Panic,
314}
315
316impl BevyError {
317 /// Returns the severity of this error.
318 pub fn severity(&self) -> Severity {
319 self.inner.severity
320 }
321
322 /// Returns this error with its severity overridden.
323 ///
324 /// Note that this doesn't change the underlying error value;
325 /// only the [`Severity`] metadata used by the error handler.
326 pub fn with_severity(mut self, severity: Severity) -> Self {
327 self.inner.severity = severity;
328 self
329 }
330
331 /// Adds a panic payload to the error.
332 /// This allows the panic to be resumed with [`Self::take_payload`] in the error handler.
333 pub fn with_payload(mut self, payload: Box<dyn Any + Send>) -> Self {
334 self.inner.panic_payload = Some(SyncCell::new(payload));
335 self
336 }
337
338 /// Use in an error handler to take the payload and use it to resume unwinding or
339 /// add it to logging.
340 pub fn take_payload(&mut self) -> Option<Box<dyn Any + Send>> {
341 self.inner.panic_payload.take().map(SyncCell::to_inner)
342 }
343}
344
345/// Extension methods for annotating errors with a [`Severity`].
346pub trait ResultSeverityExt<T, E>: Sized {
347 /// Overrides the [`Severity`] of the error if this result is `Err`.
348 /// This does not change control flow; it only annotates the error.
349 ///
350 /// # Example
351 /// ```
352 /// # use bevy_ecs::error::{BevyError, ResultSeverityExt, Severity};
353 /// fn fallible() -> Result<(), BevyError> {
354 /// // This failure is expected in some contexts, so we downgrade its severity.
355 /// let _parsed: usize = "I am not a number"
356 /// .parse()
357 /// .with_severity(Severity::Warning)?;
358 /// Ok(())
359 /// }
360 /// ```
361 ///
362 /// For more fine grained control see [`Result::map_severity`](ResultSeverityExt::map_severity)
363 fn with_severity(self, severity: Severity) -> Result<T, BevyError>;
364
365 /// Overrides the [`Severity`] of the error if this result is `Err`.
366 /// This does not change control flow; it only annotates the error.
367 ///
368 /// # Example
369 /// ```
370 /// # use bevy_ecs::error::{BevyError, ResultSeverityExt, Severity};
371 /// # use thiserror::Error;
372 /// # fn validate(_string: &str) -> Result<usize, ValidationError> {
373 /// # Err(ValidationError::IncorrectVersion)
374 /// # }
375 ///
376 /// #[derive(Error, Debug)]
377 /// pub enum ValidationError {
378 /// #[error("Incorrect version")]
379 /// IncorrectVersion,
380 /// #[error("Syntax error")]
381 /// SyntaxError,
382 /// }
383 ///
384 /// fn fallible() -> Result<(), BevyError> {
385 /// // This failure is expected in some contexts, so we downgrade its severity.
386 /// let _parsed: usize = validate("I am not a number")
387 /// .map_severity(|e| match e {
388 /// ValidationError::IncorrectVersion => Severity::Debug,
389 /// ValidationError::SyntaxError => Severity::Error,
390 /// })?;
391 /// Ok(())
392 /// }
393 /// ```
394 ///
395 /// If you don't need to inspect the error, use [`Result::with_severity`](ResultSeverityExt::with_severity)
396 fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError>;
397
398 /// Overrides the severity of the error with [`Severity::Ignore`]. See [`Result::with_severity`]
399 ///
400 /// This is shorthand for `self.with_severity(Severity::Ignore)`
401 fn ignore(self) -> Result<T, BevyError> {
402 self.with_severity(Severity::Ignore)
403 }
404
405 /// Overrides the severity of the error with [`Severity::Trace`]. See [`Result::with_severity`]
406 ///
407 /// This is shorthand for `self.with_severity(Severity::Trace)`
408 fn trace(self) -> Result<T, BevyError> {
409 self.with_severity(Severity::Trace)
410 }
411
412 /// Overrides the severity of the error with [`Severity::Info`]. See [`Result::with_severity`]
413 ///
414 /// This is shorthand for `self.with_severity(Severity::Info)`
415 fn info(self) -> Result<T, BevyError> {
416 self.with_severity(Severity::Info)
417 }
418
419 /// Overrides the severity of the error with [`Severity::Warning`]. See [`Result::with_severity`]
420 ///
421 /// This is shorthand for `self.with_severity(Severity::Warning)`
422 fn warn(self) -> Result<T, BevyError> {
423 self.with_severity(Severity::Warning)
424 }
425
426 /// Overrides the severity of the error with [`Severity::Error`]. See [`Result::with_severity`]
427 ///
428 /// This is shorthand for `self.with_severity(Severity::Error)`
429 fn error(self) -> Result<T, BevyError> {
430 self.with_severity(Severity::Error)
431 }
432
433 /// Overrides the severity of the error with [`Severity::Panic`]. See [`Result::with_severity`]
434 ///
435 /// This is shorthand for `self.with_severity(Severity::Panic)`
436 fn panic(self) -> Result<T, BevyError> {
437 self.with_severity(Severity::Panic)
438 }
439}
440
441impl<T, E> ResultSeverityExt<T, E> for Result<T, E>
442where
443 E: Into<BevyError>,
444{
445 fn with_severity(self, severity: Severity) -> Result<T, BevyError> {
446 self.map_err(|e| e.into().with_severity(severity))
447 }
448
449 fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError> {
450 self.map_err(|e| {
451 let severity = f(&e);
452 e.into().with_severity(severity)
453 })
454 }
455}
456
457/// Extension methods for adding additional context messages to a [`BevyError`]
458pub trait ContextExt<T>: Sized {
459 /// Annotate the error with a context message.
460 ///
461 /// # Example
462 /// ```
463 /// # use bevy_ecs::error::{BevyError, ContextExt};
464 /// fn fallible() -> Result<(), BevyError> {
465 /// // Produces a `BevyError` with the message
466 /// // "failed to parse number: invalid digit found in string"
467 /// let _parsed: usize = "I am not a number"
468 /// .parse()
469 /// .context("failed to parse number")?;
470 ///
471 /// Ok(())
472 /// }
473 /// ```
474 fn context<C>(self, context: C) -> Result<T, BevyError>
475 where
476 C: Into<Cow<'static, str>>,
477 {
478 self.with_context(move || context)
479 }
480
481 /// Annotate the error with a context message from a closure
482 ///
483 /// # Example
484 /// ```
485 /// # use bevy_ecs::error::{BevyError, ContextExt};
486 /// # use std::fs;
487 /// fn fallible() -> Result<(), BevyError> {
488 /// let path = "some_file.txt";
489 /// let _message = fs::read_to_string(path)
490 /// .with_context(|| format!("failed to read {path}"))?;
491 ///
492 /// Ok(())
493 /// }
494 /// ```
495 fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
496 where
497 C: Into<Cow<'static, str>>;
498}
499impl<T, E> ContextExt<T> for Result<T, E>
500where
501 E: Into<BevyError>,
502{
503 fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
504 where
505 C: Into<Cow<'static, str>>,
506 {
507 match self {
508 Ok(v) => Ok(v),
509 Err(error) => {
510 let mut error = error.into();
511 let message = context().into();
512 error.inner.context.push(message);
513 Err(error)
514 }
515 }
516 }
517}
518
519impl<T> ContextExt<T> for Option<T> {
520 fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
521 where
522 C: Into<Cow<'static, str>>,
523 {
524 match self {
525 Some(v) => Ok(v),
526 None => {
527 let message = context().into();
528
529 Err(message.into())
530 }
531 }
532 }
533}
534
535// NOTE: writing the impl this way gives us From<&str> ... nice!
536impl<E> From<E> for BevyError
537where
538 Box<dyn Error + Send + Sync + 'static>: From<E>,
539{
540 #[cold]
541 fn from(error: E) -> Self {
542 BevyError {
543 inner: Box::new(InnerBevyError {
544 error: error.into(),
545 severity: Severity::Panic,
546 context: alloc::vec![],
547 panic_payload: None,
548 #[cfg(feature = "backtrace")]
549 backtrace: std::backtrace::Backtrace::capture(),
550 }),
551 }
552 }
553}
554
555impl Display for BevyError {
556 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
557 match &self.inner.context {
558 context if context.is_empty() => writeln!(f, "{}", self.inner.error)?,
559 context if context.len() == 1 => {
560 writeln!(f, "{}: {}", context[0].trim(), self.inner.error)?;
561 }
562 context => {
563 // The most recent message is the last one in the `Vec`
564 // so we need to reverse the iterator
565 let mut reversed = context.iter().rev();
566 let first = reversed.next().unwrap().trim();
567
568 writeln!(f, "{first}\n\nCaused by:")?;
569 for message in reversed {
570 let message = message.trim();
571 writeln!(f, "\t{message}")?;
572 }
573 writeln!(f, "\t{}", self.inner.error)?;
574 }
575 }
576 self.format_backtrace(f)?;
577 Ok(())
578 }
579}
580
581impl Debug for BevyError {
582 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583 writeln!(f, "{:?}", self.inner.error)?;
584 if !self.inner.context.is_empty() {
585 writeln!(f, "context: {:?}", self.inner.context)?;
586 }
587 self.format_backtrace(f)?;
588 Ok(())
589 }
590}
591
592#[cfg(feature = "backtrace")]
593const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
594
595#[cfg(feature = "backtrace")]
596std::thread_local! {
597 static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
598 const { core::cell::Cell::new(false) };
599}
600
601/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.
602#[cfg(feature = "backtrace")]
603#[expect(clippy::print_stdout, reason = "Allowed behind `std` feature gate.")]
604pub fn bevy_error_panic_hook(
605 current_hook: impl Fn(&std::panic::PanicHookInfo),
606) -> impl Fn(&std::panic::PanicHookInfo) {
607 move |info| {
608 if SKIP_NORMAL_BACKTRACE.replace(false) {
609 if let Some(payload) = info.payload_as_str() {
610 std::println!("{payload}");
611 }
612 return;
613 }
614
615 current_hook(info);
616 }
617}
618
619/// Creates a new [`BevyError`] from a string.
620///
621/// Strings can be formatted like the [`format!`](std::format!) macro. Severity
622/// can optionally be provided to change it from the default [`Severity::Panic`].
623/// This can be done by adding the severity as the fist argument.
624///
625/// # Example
626/// ```
627/// use bevy_ecs::{bevy_error, error::{BevyError, Severity}};
628///
629/// fn this_will_fail(value: u32) -> Result<(), BevyError> {
630/// if value == 0 {
631/// return Err(bevy_error!(Severity::Debug, "A debug message"));
632/// } else {
633/// return Err(bevy_error!("We can even do formatting {value}, {}", "hello"));
634/// }
635/// }
636/// ```
637#[macro_export]
638macro_rules! bevy_error {
639 ($fmt:literal) => {
640 $crate::error::BevyError::new($crate::error::Severity::Panic, $fmt)
641 };
642 ($fmt:literal, $($arg:tt)*) => {
643 $crate::error::BevyError::new($crate::error::Severity::Panic, $crate::__macro_exports::format!($fmt, $($arg)*))
644 };
645 ($severity:expr, $fmt:literal) => {
646 $crate::error::BevyError::new($severity, $fmt)
647 };
648 ($severity:expr, $fmt:literal, $($arg:tt)*) => {
649 $crate::error::BevyError::new($severity, $crate::__macro_exports::format!($fmt, $($arg)*))
650 };
651 ($severity:expr) => {
652 compile_error!("missing error message")
653 };
654}
655
656/// Returns early with an error.
657///
658/// Equivalent to <code>return Err([bevy_error!(\...)](bevy_error!))</code>
659/// As a result the returned error defaults to [`Severity::Panic`]. As with
660/// `bevy_error!` the severity can be changed by providing a severity as the
661/// first argument. To return early only when a condition is false, use
662/// [`ensure!`](crate::ensure!).
663///
664/// # Example
665/// ```
666/// use bevy_ecs::{bail, error::{BevyError, Severity}};
667///
668/// fn do_some_stuff(val: i32) -> Result<(), BevyError> {
669/// if val < 0 {
670/// bail!(Severity::Warning, "Something is broken: {}", val);
671/// } else if val == 0 {
672/// bail!("Value really can't be zero");
673/// }
674///
675/// // ...
676/// Ok(())
677/// }
678/// ```
679#[macro_export]
680macro_rules! bail {
681 ($($args:tt)+) => {
682 return core::result::Result::Err($crate::bevy_error!($($args)*))
683 };
684}
685
686/// Returns early with an error if a condition is false.
687///
688/// Equivalent to <code>if !condition { [bail!](bail!)(\...) }</code>. As with
689/// [`bail!`], the returned error defaults to [`Severity::Panic`], and the
690/// severity can be changed by providing it after the condition.
691///
692/// # Example
693/// ```
694/// use bevy_ecs::{ensure, error::{BevyError, Severity}};
695///
696/// fn validate_score(score: i32) -> Result<(), BevyError> {
697/// ensure!(score >= 0, "score must not be negative: {}", score);
698/// ensure!(score <= 100, Severity::Warning, "score is too high: {}", score);
699/// Ok(())
700/// }
701/// ```
702#[macro_export]
703macro_rules! ensure {
704 ($condition:expr, $($args:tt)+) => {
705 if !$condition {
706 $crate::bail!($($args)*);
707 }
708 };
709}
710
711#[cfg(test)]
712mod tests {
713 use crate::error::BevyError;
714 use crate::error::ContextExt;
715 use alloc::string::ToString;
716
717 #[test]
718 #[cfg(not(miri))] // miri backtraces are weird
719 #[cfg(not(windows))] // the windows backtrace in this context is ... unhelpful and not worth testing
720 fn filtered_backtrace_test() {
721 fn i_fail() -> crate::error::Result {
722 let _: usize = "I am not a number".parse()?;
723 Ok(())
724 }
725
726 let capture_backtrace = std::env::var_os("RUST_BACKTRACE");
727
728 if capture_backtrace.is_none() || capture_backtrace.clone().is_some_and(|s| s == "0") {
729 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.")
730 }
731
732 let error = i_fail().err().unwrap();
733 let debug_message = alloc::format!("{error:?}");
734 let mut lines = debug_message.lines().peekable();
735 assert_eq!(
736 "ParseIntError { kind: InvalidDigit }",
737 lines.next().unwrap()
738 );
739
740 // On mac backtraces can start with Backtrace::create
741 // Rust 1.95 changed the format to use angle brackets: <std::backtrace::Backtrace>::create
742 // Rust 1.98 stopped inlining create into capture, so more than one of these frames can appear
743 while lines.peek().is_some_and(|line| {
744 let symbol = line.get(6..).unwrap_or("");
745 symbol.starts_with("std::backtrace::Backtrace::")
746 || symbol.starts_with("<std::backtrace::Backtrace>::")
747 }) {
748 lines.next().unwrap();
749 }
750
751 let expected_lines = alloc::vec![
752 "<bevy_ecs::error::bevy_error::BevyError as core::convert::From<core::num::error::ParseIntError>>::from",
753 "<core::result::Result<(), bevy_ecs::error::bevy_error::BevyError> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible, core::num::error::ParseIntError>>>::from_residual",
754 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail",
755 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test",
756 "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{closure#0}",
757 "<bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{closure#0} as core::ops::function::FnOnce<()>>::call_once",
758 ];
759
760 for expected in expected_lines {
761 // On mac, it can sometimes start with an "at" line
762 let mut skip = false;
763 if let Some(line) = lines.peek()
764 && line.starts_with(" at")
765 {
766 skip = true;
767 }
768
769 if skip {
770 lines.next().unwrap();
771 }
772
773 let line = lines.next().unwrap();
774 assert_eq!(&line[6..], expected);
775 }
776 // To handle any potential "at" line after the expected lines
777 let mut skip = false;
778 if let Some(line) = lines.peek()
779 && line.starts_with(" at")
780 {
781 skip = true;
782 }
783
784 if skip {
785 lines.next().unwrap();
786 }
787
788 // on linux there is a second call_once
789 let mut skip = false;
790 if let Some(line) = lines.peek()
791 && line.get(6..) == Some("<fn() -> core::result::Result<(), alloc::string::String> as core::ops::function::FnOnce<()>>::call_once")
792 {
793 skip = true;
794 }
795
796 if skip {
797 lines.next().unwrap();
798 }
799 let mut skip = false;
800 if let Some(line) = lines.peek()
801 && line.starts_with(" at")
802 {
803 skip = true;
804 }
805
806 if skip {
807 lines.next().unwrap();
808 }
809 assert_eq!(super::FILTER_MESSAGE, lines.next().unwrap());
810 assert!(lines.next().is_none());
811 }
812
813 #[test]
814 fn downcasting() {
815 #[derive(Debug, PartialEq)]
816 struct Fun(i32);
817
818 impl core::fmt::Display for Fun {
819 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
820 core::fmt::Debug::fmt(&self, f)
821 }
822 }
823 impl core::error::Error for Fun {}
824
825 let new_error = BevyError::new(crate::error::Severity::Debug, Fun(1));
826
827 assert!(new_error.is::<Fun>());
828 assert_eq!(new_error.downcast_ref::<Fun>(), Some(&Fun(1)));
829 }
830
831 /// Testing the functionality would be difficult so we at least check if it
832 /// compiles.
833 #[test]
834 fn bevy_error_macro() {
835 bevy_error!("One arg");
836 bevy_error!(crate::error::Severity::Debug, "With severity");
837 bevy_error!(
838 crate::error::Severity::Debug,
839 "With severity and args {}",
840 4 / 3
841 );
842
843 // This is the pain in the ass one since both args are literals but neither is severity
844 bevy_error!("Format string {}", 1 + 2);
845 }
846
847 #[test]
848 fn bevy_bail_macro() {
849 // Simplest way to specify the return type
850 fn t(f: impl Fn() -> Result<(), BevyError>) {
851 let val = f();
852
853 assert!(val.is_err(), "expected error got {:?}", val);
854 }
855
856 t(|| bail!("One arg"));
857 t(|| bail!(crate::error::Severity::Debug, "With severity"));
858 t(|| {
859 bail!(
860 crate::error::Severity::Debug,
861 "With severity and args {}",
862 2
863 )
864 });
865 t(|| bail!("Format string {}", 1 + 2));
866 }
867
868 #[test]
869 fn bevy_ensure_macro() {
870 fn validate(value: i32) -> Result<(), BevyError> {
871 ensure!(value != 0, "value must not be zero");
872 ensure!(
873 value > 0,
874 crate::error::Severity::Warning,
875 "value must be positive: {}",
876 value
877 );
878 Ok(())
879 }
880
881 assert!(validate(1).is_ok());
882
883 let zero = validate(0).unwrap_err();
884 assert_eq!(zero.severity(), crate::error::Severity::Panic);
885 assert!(zero.to_string().starts_with("value must not be zero"));
886
887 let negative = validate(-1).unwrap_err();
888 assert_eq!(negative.severity(), crate::error::Severity::Warning);
889 assert!(negative
890 .to_string()
891 .starts_with("value must be positive: -1"));
892 }
893
894 #[test]
895 fn context() {
896 let empty = None::<i32>;
897 let as_result = empty.context("Didn't have anything!");
898 assert!(as_result
899 .unwrap_err()
900 .to_string()
901 .starts_with("Didn't have anything!\n"));
902
903 let err: Result<i32, BevyError> =
904 Err(BevyError::new(crate::error::Severity::Debug, "Oh no!"));
905 let mut with_context = err.context("Failed");
906
907 assert!(with_context
908 .as_ref()
909 .unwrap_err()
910 .to_string()
911 .starts_with("Failed: Oh no!\n"));
912
913 with_context = with_context.context("Something went wrong");
914 assert!(with_context.unwrap_err().to_string().starts_with(
915 "Something went wrong
916
917Caused by:
918\tFailed
919\tOh no!
920"
921 ));
922 }
923
924 #[test]
925 fn context_downcasting() {
926 #[derive(Debug, PartialEq)]
927 struct Fun(i32);
928
929 impl core::fmt::Display for Fun {
930 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
931 core::fmt::Debug::fmt(&self, f)
932 }
933 }
934 impl core::error::Error for Fun {}
935
936 let fun: Result<i32, Fun> = Err(Fun(1));
937 let new_error = fun.context("Hello world!");
938
939 assert!(new_error.as_ref().unwrap_err().is::<Fun>());
940 assert_eq!(
941 new_error.as_ref().unwrap_err().downcast_ref::<Fun>(),
942 Some(&Fun(1))
943 );
944
945 let new_new_error = new_error.context("Hey there!");
946
947 assert!(new_new_error.as_ref().unwrap_err().is::<Fun>());
948 assert_eq!(
949 new_new_error.as_ref().unwrap_err().downcast_ref::<Fun>(),
950 Some(&Fun(1))
951 );
952 }
953}