Path: blob/main/crates/bevy_ecs/src/error/bevy_error.rs
6604 views
use alloc::boxed::Box;1use core::{2error::Error,3fmt::{Debug, Display},4};56/// The built in "universal" Bevy error type. This has a blanket [`From`] impl for any type that implements Rust's [`Error`],7/// meaning it can be used as a "catch all" error.8///9/// # Backtraces10///11/// When used with the `backtrace` Cargo feature, it will capture a backtrace when the error is constructed (generally in the [`From`] impl]).12/// When printed, the backtrace will be displayed. By default, the backtrace will be trimmed down to filter out noise. To see the full backtrace,13/// set the `BEVY_BACKTRACE=full` environment variable.14///15/// # Usage16///17/// ```18/// # use bevy_ecs::prelude::*;19///20/// fn fallible_system() -> Result<(), BevyError> {21/// // This will result in Rust's built-in ParseIntError, which will automatically22/// // be converted into a BevyError.23/// let parsed: usize = "I am not a number".parse()?;24/// Ok(())25/// }26/// ```27pub struct BevyError {28inner: Box<InnerBevyError>,29}3031impl BevyError {32/// Attempts to downcast the internal error to the given type.33pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {34self.inner.error.downcast_ref::<E>()35}3637fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {38#[cfg(feature = "backtrace")]39{40let f = _f;41let backtrace = &self.inner.backtrace;42if let std::backtrace::BacktraceStatus::Captured = backtrace.status() {43let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full");4445let backtrace_str = alloc::string::ToString::to_string(backtrace);46let mut skip_next_location_line = false;47for line in backtrace_str.split('\n') {48if !full_backtrace {49if skip_next_location_line {50if line.starts_with(" at") {51continue;52}53skip_next_location_line = false;54}55if line.contains("std::backtrace_rs::backtrace::") {56skip_next_location_line = true;57continue;58}59if line.contains("std::backtrace::Backtrace::") {60skip_next_location_line = true;61continue;62}63if line.contains("<bevy_ecs::error::bevy_error::BevyError as core::convert::From<E>>::from") {64skip_next_location_line = true;65continue;66}67if line.contains("<core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual") {68skip_next_location_line = true;69continue;70}71if line.contains("__rust_begin_short_backtrace") {72break;73}74if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") {75break;76}77}78writeln!(f, "{line}")?;79}80if !full_backtrace {81if std::thread::panicking() {82SKIP_NORMAL_BACKTRACE.set(true);83}84writeln!(f, "{FILTER_MESSAGE}")?;85}86}87}88Ok(())89}90}9192/// This type exists (rather than having a `BevyError(Box<dyn InnerBevyError)`) to make [`BevyError`] use a "thin pointer" instead of93/// 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".94/// We don't need to optimize it to that degree.95/// 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 simplicity96/// of the current impl is nice.97struct InnerBevyError {98error: Box<dyn Error + Send + Sync + 'static>,99#[cfg(feature = "backtrace")]100backtrace: std::backtrace::Backtrace,101}102103// NOTE: writing the impl this way gives us From<&str> ... nice!104impl<E> From<E> for BevyError105where106Box<dyn Error + Send + Sync + 'static>: From<E>,107{108#[cold]109fn from(error: E) -> Self {110BevyError {111inner: Box::new(InnerBevyError {112error: error.into(),113#[cfg(feature = "backtrace")]114backtrace: std::backtrace::Backtrace::capture(),115}),116}117}118}119120impl Display for BevyError {121fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {122writeln!(f, "{}", self.inner.error)?;123self.format_backtrace(f)?;124Ok(())125}126}127128impl Debug for BevyError {129fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {130writeln!(f, "{:?}", self.inner.error)?;131self.format_backtrace(f)?;132Ok(())133}134}135136#[cfg(feature = "backtrace")]137const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";138139#[cfg(feature = "backtrace")]140std::thread_local! {141static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =142const { core::cell::Cell::new(false) };143}144145/// When called, this will skip the currently configured panic hook when a [`BevyError`] backtrace has already been printed.146#[cfg(feature = "backtrace")]147#[expect(clippy::print_stdout, reason = "Allowed behind `std` feature gate.")]148pub fn bevy_error_panic_hook(149current_hook: impl Fn(&std::panic::PanicHookInfo),150) -> impl Fn(&std::panic::PanicHookInfo) {151move |info| {152if SKIP_NORMAL_BACKTRACE.replace(false) {153if let Some(payload) = info.payload().downcast_ref::<&str>() {154std::println!("{payload}");155} else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {156std::println!("{payload}");157}158return;159}160161current_hook(info);162}163}164165#[cfg(test)]166mod tests {167168#[test]169#[cfg(not(miri))] // miri backtraces are weird170#[cfg(not(windows))] // the windows backtrace in this context is ... unhelpful and not worth testing171fn filtered_backtrace_test() {172fn i_fail() -> crate::error::Result {173let _: usize = "I am not a number".parse()?;174Ok(())175}176177// SAFETY: this is not safe ... this test could run in parallel with another test178// that writes the environment variable. We either accept that so we can write this test,179// or we don't.180181unsafe { std::env::set_var("RUST_BACKTRACE", "1") };182183let error = i_fail().err().unwrap();184let debug_message = alloc::format!("{error:?}");185let mut lines = debug_message.lines().peekable();186assert_eq!(187"ParseIntError { kind: InvalidDigit }",188lines.next().unwrap()189);190191// On mac backtraces can start with Backtrace::create192let mut skip = false;193if let Some(line) = lines.peek() {194if &line[6..] == "std::backtrace::Backtrace::create" {195skip = true;196}197}198199if skip {200lines.next().unwrap();201}202203let expected_lines = alloc::vec![204"bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail",205"bevy_ecs::error::bevy_error::tests::filtered_backtrace_test",206"bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{{closure}}",207"core::ops::function::FnOnce::call_once",208];209210for expected in expected_lines {211let line = lines.next().unwrap();212assert_eq!(&line[6..], expected);213let mut skip = false;214if let Some(line) = lines.peek() {215if line.starts_with(" at") {216skip = true;217}218}219220if skip {221lines.next().unwrap();222}223}224225// on linux there is a second call_once226let mut skip = false;227if let Some(line) = lines.peek() {228if &line[6..] == "core::ops::function::FnOnce::call_once" {229skip = true;230}231}232if skip {233lines.next().unwrap();234}235let mut skip = false;236if let Some(line) = lines.peek() {237if line.starts_with(" at") {238skip = true;239}240}241242if skip {243lines.next().unwrap();244}245assert_eq!(super::FILTER_MESSAGE, lines.next().unwrap());246assert!(lines.next().is_none());247}248}249250251