use alloc::boxed::Box;
use core::{
error::Error,
fmt::{Debug, Display},
};
pub struct BevyError {
inner: Box<InnerBevyError>,
}
impl BevyError {
pub fn new<E>(severity: Severity, error: E) -> Self
where
Box<dyn Error + Sync + Send>: From<E>,
{
Self::from(error).with_severity(severity)
}
pub fn ignore<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Ignore, error)
}
pub fn trace<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Trace, error)
}
pub fn debug<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Debug, error)
}
pub fn info<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Info, error)
}
pub fn warning<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Warning, error)
}
pub fn error<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Error, error)
}
pub fn panic<E>(error: E) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Panic, error)
}
pub fn is<E: Error + 'static>(&self) -> bool {
self.inner.error.is::<E>()
}
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>,
severity: Severity,
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
pub enum Severity {
Ignore,
Trace,
Debug,
Info,
Warning,
Error,
Panic,
}
impl BevyError {
pub fn severity(&self) -> Severity {
self.inner.severity
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.inner.severity = severity;
self
}
}
pub trait ResultSeverityExt<T, E>: Sized {
fn with_severity(self, severity: Severity) -> Result<T, BevyError>;
fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError>;
fn ignore(self) -> Result<T, BevyError> {
self.with_severity(Severity::Ignore)
}
fn trace(self) -> Result<T, BevyError> {
self.with_severity(Severity::Trace)
}
fn info(self) -> Result<T, BevyError> {
self.with_severity(Severity::Info)
}
fn warn(self) -> Result<T, BevyError> {
self.with_severity(Severity::Warning)
}
fn error(self) -> Result<T, BevyError> {
self.with_severity(Severity::Error)
}
fn panic(self) -> Result<T, BevyError> {
self.with_severity(Severity::Panic)
}
}
impl<T, E> ResultSeverityExt<T, E> for Result<T, E>
where
E: Into<BevyError>,
{
fn with_severity(self, severity: Severity) -> Result<T, BevyError> {
self.map_err(|e| e.into().with_severity(severity))
}
fn map_severity(self, f: impl FnOnce(&E) -> Severity) -> Result<T, BevyError> {
self.map_err(|e| {
let severity = f(&e);
e.into().with_severity(severity)
})
}
}
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(),
severity: Severity::Panic,
#[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 {
use crate::error::BevyError;
#[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(())
}
let capture_backtrace = std::env::var_os("RUST_BACKTRACE");
if capture_backtrace.is_none() || capture_backtrace.clone().is_some_and(|s| s == "0") {
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.")
}
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"
|| 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());
}
#[test]
fn 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 new_error = BevyError::new(crate::error::Severity::Debug, Fun(1));
assert!(new_error.is::<Fun>());
assert_eq!(new_error.downcast_ref::<Fun>(), Some(&Fun(1)));
}
}