From 0d658232e88879b4a0e0d04068f741914b87f868 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:30:45 +1000 Subject: [PATCH 1/9] Improve `BevyError` ergonomics Add a `context` method to `Result` and `Option` and some extra utilities to `ResultSeverityExt` --- crates/bevy_ecs/src/error/bevy_error.rs | 224 +++++++++++++++++++++++- crates/bevy_ecs/src/lib.rs | 2 +- 2 files changed, 216 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index c9f06796beaca..37fd1fa0c5896 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -1,4 +1,4 @@ -use alloc::boxed::Box; +use alloc::{boxed::Box, string::ToString}; use core::{ error::Error, fmt::{Debug, Display}, @@ -192,6 +192,11 @@ impl BevyError { self.inner.error.downcast_ref::() } + /// Attempts to downcast the internal error to a mutable reference to the given type. + pub fn downcast_mut(&mut self) -> Option<&mut E> { + self.inner.error.downcast_mut::() + } + fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { #[cfg(feature = "backtrace")] { @@ -201,7 +206,7 @@ impl BevyError { // TODO: Cache let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full"); - let backtrace_str = alloc::string::ToString::to_string(backtrace); + let backtrace_str = backtrace.to_string(); let mut skip_next_location_line = false; for line in backtrace_str.split('\n') { if !full_backtrace { @@ -219,18 +224,22 @@ impl BevyError { skip_next_location_line = true; continue; } - if line.contains(">::from") { + if line.contains( + "bevy_ecs::error::bevy_error::BevyError as core::convert::From", + ) { skip_next_location_line = true; continue; } - if line.contains(" as core::ops::try_trait::FromResidual>>::from_residual") { + if line.contains("core::ops::try_trait::FromResidual") + && line.contains("::from_residual") + { skip_next_location_line = true; continue; } if line.contains("__rust_begin_short_backtrace") { break; } - if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") { + if line.contains("bevy_ecs::observer::Observers::invoke::{closure") { break; } } @@ -311,7 +320,7 @@ impl BevyError { } /// Extension methods for annotating errors with a [`Severity`]. -pub trait ResultSeverityExt { +pub trait ResultSeverityExt: Sized { /// Overrides the [`Severity`] of the error if this result is `Err`. /// This does not change control flow; it only annotates the error. /// @@ -362,6 +371,48 @@ pub trait ResultSeverityExt { /// /// If you don't need to inspect the error, use [`Result::with_severity`](ResultSeverityExt::with_severity) fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result; + + /// Overrides the severity of the error with [`Severity::Ignore`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Ignore)` + fn ignore(self) -> Result { + self.with_severity(Severity::Ignore) + } + + /// Overrides the severity of the error with [`Severity::Trace`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Trace)` + fn trace(self) -> Result { + self.with_severity(Severity::Trace) + } + + /// Overrides the severity of the error with [`Severity::Info`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Info)` + fn info(self) -> Result { + self.with_severity(Severity::Info) + } + + /// Overrides the severity of the error with [`Severity::Warning`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Warning)` + fn warn(self) -> Result { + self.with_severity(Severity::Warning) + } + + /// Overrides the severity of the error with [`Severity::Error`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Error)` + fn error(self) -> Result { + self.with_severity(Severity::Error) + } + + /// Overrides the severity of the error with [`Severity::Panic`]. See [`Result::with_severity`] + /// + /// This is shorthand for `self.with_severity(Severity::Panic)` + fn panic(self) -> Result { + self.with_severity(Severity::Panic) + } } impl ResultSeverityExt for Result @@ -380,6 +431,129 @@ where } } +/// Extension methods for adding additional context messages to a [`BevyError`] +pub trait ContextExt: Sized { + /// Annotate the error with a context message. + /// + /// # Example + /// ``` + /// # use bevy_ecs::error::{BevyError, ContextExt}; + /// fn fallible() -> Result<(), BevyError> { + /// // Produces a `BevyError` with the message + /// // "failed to parse number: invalid digit found in string" + /// let _parsed: usize = "I am not a number" + /// .parse() + /// .context("failed to parse number")?; + /// + /// Ok(()) + /// } + /// ``` + fn context(self, context: C) -> Result + where + C: Display, + { + self.with_context(move || context) + } + + /// Annotate the error with a context message from a closure + /// + /// # Example + /// ``` + /// # use bevy_ecs::error::{BevyError, ContextExt}; + /// # use std::fs; + /// fn fallible() -> Result<(), BevyError> { + /// let path = "some_file.txt"; + /// let _message = fs::read_to_string(path) + /// .with_context(|| format!("failed to read {path}"))?; + /// + /// Ok(()) + /// } + /// ``` + fn with_context(self, context: impl FnOnce() -> C) -> Result + where + C: Display; +} + +impl ContextExt for Result +where + E: Into, +{ + fn with_context(self, context: impl FnOnce() -> C) -> Result + where + C: Display, + { + match self { + Ok(v) => Ok(v), + Err(error) => { + let mut error = error.into(); + let msg = context().to_string(); + if let Some(ctx) = error.downcast_mut::() { + ctx.messages.push(msg); + Err(error) + } else { + let error = ContextError { + messages: alloc::vec![error.to_string(), msg], + }; + + Err(error.into()) + } + } + } + } +} + +impl ContextExt for Option { + fn with_context(self, context: impl FnOnce() -> C) -> Result + where + C: Display, + { + match self { + Some(v) => Ok(v), + None => { + let error = ContextError { + messages: alloc::vec![context().to_string()], + }; + + Err(error.into()) + } + } + } +} + +#[derive(Debug, PartialEq, Clone)] +struct ContextError { + messages: alloc::vec::Vec, +} + +impl Display for ContextError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match &self.messages { + messages if messages.len() == 2 => { + write!(f, "{}: {}", messages[1].trim(), messages[0].trim())?; + } + messages if messages.len() == 1 => write!(f, "{}", messages[0].trim())?, + messages if messages.is_empty() => {} // nop + messages => { + // The most recent message is the last one in the `Vec` + // so we need to reverse the iterator + let mut reversed = messages.iter().rev(); + // `messages.len()` is at least 3 + let first = reversed.next().unwrap().trim(); + + write!(f, "{first}\n\nCaused by:")?; + for message in reversed { + let message = message.trim(); + write!(f, "\n\t{message}")?; + } + } + } + + Ok(()) + } +} + +impl Error for ContextError {} + // NOTE: writing the impl this way gives us From<&str> ... nice! impl From for BevyError where @@ -510,6 +684,8 @@ macro_rules! bail { #[cfg(test)] mod tests { use crate::error::BevyError; + use crate::error::ContextExt; + use alloc::string::ToString; #[test] #[cfg(not(miri))] // miri backtraces are weird @@ -551,8 +727,8 @@ mod tests { let expected_lines = alloc::vec![ "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail", "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test", - "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{{closure}}", - "core::ops::function::FnOnce::call_once", + "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{closure#0}", + ">::call_once", ]; for expected in expected_lines { @@ -573,7 +749,7 @@ mod tests { // on linux there is a second call_once let mut skip = false; if let Some(line) = lines.peek() - && &line[6..] == "core::ops::function::FnOnce::call_once" + && line.contains("core::ops::function::FnOnce<()>") { skip = true; } @@ -649,4 +825,34 @@ mod tests { }); t(|| bail!("Format string {}", 1 + 2)); } + + #[test] + fn context() { + let empty = None::; + let as_result = empty.context("Didn't have anything!"); + assert!(as_result + .unwrap_err() + .to_string() + .starts_with("Didn't have anything!\n")); + + let err: Result = + Err(BevyError::new(crate::error::Severity::Debug, "Oh no!")); + let mut with_context = err.context("Failed"); + + assert!(with_context + .as_ref() + .unwrap_err() + .to_string() + .starts_with("Failed: Oh no!\n")); + + with_context = with_context.context("Something went wrong"); + assert!(with_context.unwrap_err().to_string().starts_with( + "Something went wrong + +Caused by: +\tFailed +\tOh no! +" + )); + } } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index dee0fc0aa0f24..4a050a7087d8c 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -74,7 +74,7 @@ pub mod prelude { children, component::Component, entity::{ContainsEntity, Entity, EntityMapper}, - error::{BevyError, Result, ResultSeverityExt, Severity}, + error::{BevyError, ContextExt, Result, ResultSeverityExt, Severity}, event::{EntityEvent, Event}, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, lifecycle::{Add, Despawn, Discard, Insert, Remove, RemovedComponents}, From 5667fff938d3dcfd519b969d0a7826dc7df6e338 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:10:01 +1000 Subject: [PATCH 2/9] Change `filter_backtrace` test to make CI happy --- crates/bevy_ecs/src/error/bevy_error.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 37fd1fa0c5896..13b2f37b172c2 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -239,7 +239,9 @@ impl BevyError { if line.contains("__rust_begin_short_backtrace") { break; } - if line.contains("bevy_ecs::observer::Observers::invoke::{closure") { + if line.contains("bevy_ecs::observer::Observers::invoke::") + && line.contains("closure") + { break; } } @@ -727,8 +729,8 @@ mod tests { let expected_lines = alloc::vec![ "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::i_fail", "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test", - "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{closure#0}", - ">::call_once", + "bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{{closure}}", + "core::ops::function::FnOnce::call_once", ]; for expected in expected_lines { @@ -749,7 +751,7 @@ mod tests { // on linux there is a second call_once let mut skip = false; if let Some(line) = lines.peek() - && line.contains("core::ops::function::FnOnce<()>") + && line.contains("core::ops::function::FnOnce") { skip = true; } From 47c644fc923fb3a4d87d4dc390d0973c5a03f384 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:52:21 +1000 Subject: [PATCH 3/9] Add a release note --- .../release-notes/bevy_error_context.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 _release-content/release-notes/bevy_error_context.md diff --git a/_release-content/release-notes/bevy_error_context.md b/_release-content/release-notes/bevy_error_context.md new file mode 100644 index 0000000000000..d6fd715ed7f61 --- /dev/null +++ b/_release-content/release-notes/bevy_error_context.md @@ -0,0 +1,50 @@ +--- +title: `BevyError` Context Messages +authors: ["@cookie1170"] +pull_requests: [24528] +--- + +Similar to the popular `anyhow` crate, `BevyError` now provides an ergonomic way to attach extra context to an error using the `context` method, +which also allows creating a `Result` from an `Option`. + +This makes it easier to trace back errors with human-readable messages without looking at verbose backtraces. + +```rs +fn fallible() -> Result<(), BevyError> { + // This produces the error message `Failed to parse number: invalid digit found in string` + let parsed: usize = "I am not a number" + .parse() + .context("Failed to parse number")?; + + Ok(()) +} +``` + +`with_context` may be used to produce the error string with a closure instead. + +If multiple `context`s were used on the same `BevyError`, they're enumerated below: + +```rs +fn fallible() -> Result { + let path = "package.json"; + let package = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {path}"))?; + + serde_json::parse(&package)? +} + +fn uses_fallible() -> Result<(), BevyError> { + let package = fallible().context("Failed to parse package.json")?; + // Use `package`... +} +``` + +Will produce the following error if `package.json` is missing: + +```rs +Failed to parse package.json + +Caused by: + Failed to read package.json + No such file or directory (os error 2) +``` From 6a11ea6a66c9eb10dfd89df2330a0a30bb2b79d4 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:35:39 +1000 Subject: [PATCH 4/9] Fix downcasting when using `context` --- crates/bevy_ecs/src/error/bevy_error.rs | 85 +++++++++++++++++++++---- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 13b2f37b172c2..d4dffab5699a8 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -184,17 +184,20 @@ impl BevyError { /// Checks if the internal error is of the given type. pub fn is(&self) -> bool { - self.inner.error.is::() + if let Some(context) = self.inner.error.downcast_ref::() { + context.error.is::() + } else { + self.inner.error.is::() + } } /// Attempts to downcast the internal error to the given type. pub fn downcast_ref(&self) -> Option<&E> { - self.inner.error.downcast_ref::() - } - - /// Attempts to downcast the internal error to a mutable reference to the given type. - pub fn downcast_mut(&mut self) -> Option<&mut E> { - self.inner.error.downcast_mut::() + if let Some(context) = self.inner.error.downcast_ref::() { + context.error.downcast_ref::() + } else { + self.inner.error.downcast_ref::() + } } fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -478,7 +481,7 @@ pub trait ContextExt: Sized { impl ContextExt for Result where - E: Into, + Box: From, { fn with_context(self, context: impl FnOnce() -> C) -> Result where @@ -487,14 +490,15 @@ where match self { Ok(v) => Ok(v), Err(error) => { - let mut error = error.into(); + let mut error: Box = error.into(); let msg = context().to_string(); if let Some(ctx) = error.downcast_mut::() { ctx.messages.push(msg); - Err(error) + Err(error.into()) } else { let error = ContextError { messages: alloc::vec![error.to_string(), msg], + error, }; Err(error.into()) @@ -504,6 +508,30 @@ where } } +impl ContextExt for Result { + fn with_context(self, context: impl FnOnce() -> C) -> Result + where + C: Display, + { + match self { + Ok(v) => Ok(v), + Err(mut error) => { + let msg = context().to_string(); + if let Some(ctx) = error.inner.error.downcast_mut::() { + ctx.messages.push(msg); + } else { + error.inner.error = ContextError { + messages: alloc::vec![error.inner.error.to_string(), msg], + error: error.inner.error, + } + .into(); + } + Err(error) + } + } + } +} + impl ContextExt for Option { fn with_context(self, context: impl FnOnce() -> C) -> Result where @@ -512,8 +540,10 @@ impl ContextExt for Option { match self { Some(v) => Ok(v), None => { + let message = context().to_string(); let error = ContextError { - messages: alloc::vec![context().to_string()], + messages: alloc::vec![message.clone()], + error: message.into(), }; Err(error.into()) @@ -522,9 +552,10 @@ impl ContextExt for Option { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug)] struct ContextError { messages: alloc::vec::Vec, + error: Box, } impl Display for ContextError { @@ -857,4 +888,34 @@ Caused by: " )); } + + #[test] + fn context_downcasting() { + #[derive(Debug, PartialEq)] + struct Fun(i32); + + impl core::fmt::Display for Fun { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self, f) + } + } + impl core::error::Error for Fun {} + + let fun: Result = Err(Fun(1)); + let new_error = fun.context("Hello world!"); + + assert!(new_error.as_ref().unwrap_err().is::()); + assert_eq!( + new_error.as_ref().unwrap_err().downcast_ref::(), + Some(&Fun(1)) + ); + + let new_new_error = new_error.context("Hey there!"); + + assert!(new_new_error.as_ref().unwrap_err().is::()); + assert_eq!( + new_new_error.as_ref().unwrap_err().downcast_ref::(), + Some(&Fun(1)) + ); + } } From e0d5eec8d586771d03a5177ad8a5d7e94ed8b414 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:38:31 +1000 Subject: [PATCH 5/9] fix release note title --- _release-content/release-notes/bevy_error_context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/release-notes/bevy_error_context.md b/_release-content/release-notes/bevy_error_context.md index d6fd715ed7f61..2959b25ad3177 100644 --- a/_release-content/release-notes/bevy_error_context.md +++ b/_release-content/release-notes/bevy_error_context.md @@ -1,5 +1,5 @@ --- -title: `BevyError` Context Messages +title: Bevy Error Context Messages authors: ["@cookie1170"] pull_requests: [24528] --- From 0a9572775c7eb80fd344b273891c6706e30453b1 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:55:18 +1000 Subject: [PATCH 6/9] fix doc test --- crates/bevy_ecs/src/error/bevy_error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index d4dffab5699a8..5396259df8359 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -446,8 +446,8 @@ pub trait ContextExt: Sized { /// fn fallible() -> Result<(), BevyError> { /// // Produces a `BevyError` with the message /// // "failed to parse number: invalid digit found in string" - /// let _parsed: usize = "I am not a number" - /// .parse() + /// let _parsed = "I am not a number" + /// .parse::() /// .context("failed to parse number")?; /// /// Ok(()) From b8ed30a9b4af3fef67f1548ebe303ca8e8f4e016 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Fri, 5 Jun 2026 07:38:22 +1000 Subject: [PATCH 7/9] remove unnecessary impl of `ContextExt` --- crates/bevy_ecs/src/error/bevy_error.rs | 34 +++---------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 5396259df8359..36c97ae6d61a5 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -446,8 +446,8 @@ pub trait ContextExt: Sized { /// fn fallible() -> Result<(), BevyError> { /// // Produces a `BevyError` with the message /// // "failed to parse number: invalid digit found in string" - /// let _parsed = "I am not a number" - /// .parse::() + /// let _parsed: usize = "I am not a number" + /// .parse() /// .context("failed to parse number")?; /// /// Ok(()) @@ -478,10 +478,9 @@ pub trait ContextExt: Sized { where C: Display; } - impl ContextExt for Result where - Box: From, + E: Into, { fn with_context(self, context: impl FnOnce() -> C) -> Result where @@ -490,32 +489,7 @@ where match self { Ok(v) => Ok(v), Err(error) => { - let mut error: Box = error.into(); - let msg = context().to_string(); - if let Some(ctx) = error.downcast_mut::() { - ctx.messages.push(msg); - Err(error.into()) - } else { - let error = ContextError { - messages: alloc::vec![error.to_string(), msg], - error, - }; - - Err(error.into()) - } - } - } - } -} - -impl ContextExt for Result { - fn with_context(self, context: impl FnOnce() -> C) -> Result - where - C: Display, - { - match self { - Ok(v) => Ok(v), - Err(mut error) => { + let mut error = error.into(); let msg = context().to_string(); if let Some(ctx) = error.inner.error.downcast_mut::() { ctx.messages.push(msg); From a85f540a3ae35ad8ea3ca4501f91547a3aebcf15 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:57:42 +1000 Subject: [PATCH 8/9] undo backtrace changes --- crates/bevy_ecs/src/error/bevy_error.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 36c97ae6d61a5..f79406594482d 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -227,24 +227,18 @@ impl BevyError { skip_next_location_line = true; continue; } - if line.contains( - "bevy_ecs::error::bevy_error::BevyError as core::convert::From", - ) { + if line.contains(">::from") { skip_next_location_line = true; continue; } - if line.contains("core::ops::try_trait::FromResidual") - && line.contains("::from_residual") - { + if line.contains(" as core::ops::try_trait::FromResidual>>::from_residual") { skip_next_location_line = true; continue; } if line.contains("__rust_begin_short_backtrace") { break; } - if line.contains("bevy_ecs::observer::Observers::invoke::") - && line.contains("closure") - { + if line.contains("bevy_ecs::observer::Observers::invoke::{{closure}}") { break; } } @@ -756,7 +750,7 @@ mod tests { // on linux there is a second call_once let mut skip = false; if let Some(line) = lines.peek() - && line.contains("core::ops::function::FnOnce") + && &line[6..] == "core::ops::function::FnOnce::call_once" { skip = true; } From 4023683054545f320e227a650a1b6095e9ee6c71 Mon Sep 17 00:00:00 2001 From: cookie1170 <171882521+cookie1170@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:26:03 +1000 Subject: [PATCH 9/9] move the context to `InnerBevyError` instead of a separate type --- crates/bevy_ecs/src/error/bevy_error.rs | 88 ++++++++----------------- 1 file changed, 27 insertions(+), 61 deletions(-) diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index f79406594482d..0ac6044d4c1f9 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -106,6 +106,7 @@ impl BevyError { inner: Box::new(InnerBevyError { error: error.into(), severity, + context: alloc::vec![], #[cfg(feature = "backtrace")] backtrace, }), @@ -184,20 +185,12 @@ impl BevyError { /// Checks if the internal error is of the given type. pub fn is(&self) -> bool { - if let Some(context) = self.inner.error.downcast_ref::() { - context.error.is::() - } else { - self.inner.error.is::() - } + self.inner.error.is::() } /// Attempts to downcast the internal error to the given type. pub fn downcast_ref(&self) -> Option<&E> { - if let Some(context) = self.inner.error.downcast_ref::() { - context.error.downcast_ref::() - } else { - self.inner.error.downcast_ref::() - } + self.inner.error.downcast_ref::() } fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -263,6 +256,7 @@ impl BevyError { /// of the current impl is nice. struct InnerBevyError { error: Box, + context: alloc::vec::Vec, severity: Severity, #[cfg(feature = "backtrace")] backtrace: std::backtrace::Backtrace, @@ -484,16 +478,8 @@ where Ok(v) => Ok(v), Err(error) => { let mut error = error.into(); - let msg = context().to_string(); - if let Some(ctx) = error.inner.error.downcast_mut::() { - ctx.messages.push(msg); - } else { - error.inner.error = ContextError { - messages: alloc::vec![error.inner.error.to_string(), msg], - error: error.inner.error, - } - .into(); - } + let message = context().to_string(); + error.inner.context.push(message); Err(error) } } @@ -509,52 +495,13 @@ impl ContextExt for Option { Some(v) => Ok(v), None => { let message = context().to_string(); - let error = ContextError { - messages: alloc::vec![message.clone()], - error: message.into(), - }; - Err(error.into()) + Err(message.into()) } } } } -#[derive(Debug)] -struct ContextError { - messages: alloc::vec::Vec, - error: Box, -} - -impl Display for ContextError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match &self.messages { - messages if messages.len() == 2 => { - write!(f, "{}: {}", messages[1].trim(), messages[0].trim())?; - } - messages if messages.len() == 1 => write!(f, "{}", messages[0].trim())?, - messages if messages.is_empty() => {} // nop - messages => { - // The most recent message is the last one in the `Vec` - // so we need to reverse the iterator - let mut reversed = messages.iter().rev(); - // `messages.len()` is at least 3 - let first = reversed.next().unwrap().trim(); - - write!(f, "{first}\n\nCaused by:")?; - for message in reversed { - let message = message.trim(); - write!(f, "\n\t{message}")?; - } - } - } - - Ok(()) - } -} - -impl Error for ContextError {} - // NOTE: writing the impl this way gives us From<&str> ... nice! impl From for BevyError where @@ -566,6 +513,7 @@ where inner: Box::new(InnerBevyError { error: error.into(), severity: Severity::Panic, + context: alloc::vec![], #[cfg(feature = "backtrace")] backtrace: std::backtrace::Backtrace::capture(), }), @@ -575,7 +523,25 @@ where impl Display for BevyError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - writeln!(f, "{}", self.inner.error)?; + match &self.inner.context { + context if context.is_empty() => writeln!(f, "{}", self.inner.error)?, + context if context.len() == 1 => { + writeln!(f, "{}: {}", context[0].trim(), self.inner.error)?; + } + context => { + // The most recent message is the last one in the `Vec` + // so we need to reverse the iterator + let error = self.inner.error.to_string(); + let mut reversed = context.iter().rev().chain(core::iter::once(&error)); + let first = reversed.next().unwrap().trim(); + + writeln!(f, "{first}\n\nCaused by:")?; + for message in reversed { + let message = message.trim(); + writeln!(f, "\t{message}")?; + } + } + } self.format_backtrace(f)?; Ok(()) }