use alloc::boxed::Box;
use core::{
error::Error,
fmt::{Debug, Display},
};
pub struct BevyError {
inner: Box<InnerBevyError>,
}
impl BevyError {
pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {
self.inner.error.downcast_ref::<E>()
}
fn format_backtrace(&self, _f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
#[cfg(feature = "backtrace")]
{
let f = _f;
let backtrace = &self.inner.backtrace;
if let std::backtrace::BacktraceStatus::Captured = backtrace.status() {
let full_backtrace = std::env::var("BEVY_BACKTRACE").is_ok_and(|val| val == "full");
let backtrace_str = alloc::string::ToString::to_string(backtrace);
let mut skip_next_location_line = false;
for line in backtrace_str.split('\n') {
if !full_backtrace {
if skip_next_location_line {
if line.starts_with(" at") {
continue;
}
skip_next_location_line = false;
}
if line.contains("std::backtrace_rs::backtrace::") {
skip_next_location_line = true;
continue;
}
if line.contains("std::backtrace::Backtrace::") {
skip_next_location_line = true;
continue;
}
if line.contains("<bevy_ecs::error::bevy_error::BevyError as core::convert::From<E>>::from") {
skip_next_location_line = true;
continue;
}
if line.contains("<core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::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}}") {
break;
}
}
writeln!(f, "{line}")?;
}
if !full_backtrace {
if std::thread::panicking() {
SKIP_NORMAL_BACKTRACE.set(true);
}
writeln!(f, "{FILTER_MESSAGE}")?;
}
}
}
Ok(())
}
}
struct InnerBevyError {
error: Box<dyn Error + Send + Sync + 'static>,
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace,
}
impl<E> From<E> for BevyError
where
Box<dyn Error + Send + Sync + 'static>: From<E>,
{
#[cold]
fn from(error: E) -> Self {
BevyError {
inner: Box::new(InnerBevyError {
error: error.into(),
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace::capture(),
}),
}
}
}
impl Display for BevyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f, "{}", self.inner.error)?;
self.format_backtrace(f)?;
Ok(())
}
}
impl Debug for BevyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
writeln!(f, "{:?}", self.inner.error)?;
self.format_backtrace(f)?;
Ok(())
}
}
#[cfg(feature = "backtrace")]
const FILTER_MESSAGE: &str = "note: Some \"noisy\" backtrace lines have been filtered out. Run with `BEVY_BACKTRACE=full` for a verbose backtrace.";
#[cfg(feature = "backtrace")]
std::thread_local! {
static SKIP_NORMAL_BACKTRACE: core::cell::Cell<bool> =
const { core::cell::Cell::new(false) };
}
#[cfg(feature = "backtrace")]
#[expect(clippy::print_stdout, reason = "Allowed behind `std` feature gate.")]
pub fn bevy_error_panic_hook(
current_hook: impl Fn(&std::panic::PanicHookInfo),
) -> impl Fn(&std::panic::PanicHookInfo) {
move |info| {
if SKIP_NORMAL_BACKTRACE.replace(false) {
if let Some(payload) = info.payload().downcast_ref::<&str>() {
std::println!("{payload}");
} else if let Some(payload) = info.payload().downcast_ref::<alloc::string::String>() {
std::println!("{payload}");
}
return;
}
current_hook(info);
}
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(not(miri))] #[cfg(not(windows))] fn filtered_backtrace_test() {
fn i_fail() -> crate::error::Result {
let _: usize = "I am not a number".parse()?;
Ok(())
}
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
let error = i_fail().err().unwrap();
let debug_message = alloc::format!("{error:?}");
let mut lines = debug_message.lines().peekable();
assert_eq!(
"ParseIntError { kind: InvalidDigit }",
lines.next().unwrap()
);
let mut skip = false;
if let Some(line) = lines.peek()
&& &line[6..] == "std::backtrace::Backtrace::create"
{
skip = true;
}
if skip {
lines.next().unwrap();
}
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",
];
for expected in expected_lines {
let line = lines.next().unwrap();
assert_eq!(&line[6..], expected);
let mut skip = false;
if let Some(line) = lines.peek()
&& line.starts_with(" at")
{
skip = true;
}
if skip {
lines.next().unwrap();
}
}
let mut skip = false;
if let Some(line) = lines.peek()
&& &line[6..] == "core::ops::function::FnOnce::call_once"
{
skip = true;
}
if skip {
lines.next().unwrap();
}
let mut skip = false;
if let Some(line) = lines.peek()
&& line.starts_with(" at")
{
skip = true;
}
if skip {
lines.next().unwrap();
}
assert_eq!(super::FILTER_MESSAGE, lines.next().unwrap());
assert!(lines.next().is_none());
}
}