use alloc::{borrow::Cow, boxed::Box};
use bevy_platform::cell::SyncCell;
use core::{
any::Any,
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)
}
#[cfg(feature = "std")]
pub fn new_with_backtrace<E>(
severity: Severity,
error: E,
backtrace: std::backtrace::Backtrace,
) -> Self
where
Box<dyn Error + Sync + Send>: From<E>,
{
#[cfg(not(feature = "backtrace"))]
drop(backtrace);
BevyError {
inner: Box::new(InnerBevyError {
error: error.into(),
severity,
context: alloc::vec![],
panic_payload: None,
#[cfg(feature = "backtrace")]
backtrace,
}),
}
}
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, payload: Box<dyn Any + Send>) -> Self
where
Box<dyn Error + Send + Sync>: From<E>,
{
Self::new(Severity::Panic, error).with_payload(payload)
}
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::")
|| 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>,
context: alloc::vec::Vec<Cow<'static, str>>,
severity: Severity,
panic_payload: Option<SyncCell<Box<dyn Any + Send>>>,
#[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 fn with_payload(mut self, payload: Box<dyn Any + Send>) -> Self {
self.inner.panic_payload = Some(SyncCell::new(payload));
self
}
pub fn take_payload(&mut self) -> Option<Box<dyn Any + Send>> {
self.inner.panic_payload.take().map(SyncCell::to_inner)
}
}
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)
})
}
}
pub trait ContextExt<T>: Sized {
fn context<C>(self, context: C) -> Result<T, BevyError>
where
C: Into<Cow<'static, str>>,
{
self.with_context(move || context)
}
fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
where
C: Into<Cow<'static, str>>;
}
impl<T, E> ContextExt<T> for Result<T, E>
where
E: Into<BevyError>,
{
fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
where
C: Into<Cow<'static, str>>,
{
match self {
Ok(v) => Ok(v),
Err(error) => {
let mut error = error.into();
let message = context().into();
error.inner.context.push(message);
Err(error)
}
}
}
}
impl<T> ContextExt<T> for Option<T> {
fn with_context<C>(self, context: impl FnOnce() -> C) -> Result<T, BevyError>
where
C: Into<Cow<'static, str>>,
{
match self {
Some(v) => Ok(v),
None => {
let message = context().into();
Err(message.into())
}
}
}
}
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,
context: alloc::vec![],
panic_payload: None,
#[cfg(feature = "backtrace")]
backtrace: std::backtrace::Backtrace::capture(),
}),
}
}
}
impl Display for BevyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
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 => {
let mut reversed = context.iter().rev();
let first = reversed.next().unwrap().trim();
writeln!(f, "{first}\n\nCaused by:")?;
for message in reversed {
let message = message.trim();
writeln!(f, "\t{message}")?;
}
writeln!(f, "\t{}", 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)?;
if !self.inner.context.is_empty() {
writeln!(f, "context: {:?}", self.inner.context)?;
}
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_as_str() {
std::println!("{payload}");
}
return;
}
current_hook(info);
}
}
#[macro_export]
macro_rules! bevy_error {
($fmt:literal) => {
$crate::error::BevyError::new($crate::error::Severity::Panic, $fmt)
};
($fmt:literal, $($arg:tt)*) => {
$crate::error::BevyError::new($crate::error::Severity::Panic, $crate::__macro_exports::format!($fmt, $($arg)*))
};
($severity:expr, $fmt:literal) => {
$crate::error::BevyError::new($severity, $fmt)
};
($severity:expr, $fmt:literal, $($arg:tt)*) => {
$crate::error::BevyError::new($severity, $crate::__macro_exports::format!($fmt, $($arg)*))
};
($severity:expr) => {
compile_error!("missing error message")
};
}
#[macro_export]
macro_rules! bail {
($($args:tt)+) => {
return core::result::Result::Err($crate::bevy_error!($($args)*))
};
}
#[macro_export]
macro_rules! ensure {
($condition:expr, $($args:tt)+) => {
if !$condition {
$crate::bail!($($args)*);
}
};
}
#[cfg(test)]
mod tests {
use crate::error::BevyError;
use crate::error::ContextExt;
use alloc::string::ToString;
#[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()
);
while lines.peek().is_some_and(|line| {
let symbol = line.get(6..).unwrap_or("");
symbol.starts_with("std::backtrace::Backtrace::")
|| symbol.starts_with("<std::backtrace::Backtrace>::")
}) {
lines.next().unwrap();
}
let expected_lines = alloc::vec![
"<bevy_ecs::error::bevy_error::BevyError as core::convert::From<core::num::error::ParseIntError>>::from",
"<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",
"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}",
"<bevy_ecs::error::bevy_error::tests::filtered_backtrace_test::{closure#0} as core::ops::function::FnOnce<()>>::call_once",
];
for expected in expected_lines {
let mut skip = false;
if let Some(line) = lines.peek()
&& line.starts_with(" at")
{
skip = true;
}
if skip {
lines.next().unwrap();
}
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.get(6..) == Some("<fn() -> core::result::Result<(), alloc::string::String> as 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)));
}
#[test]
fn bevy_error_macro() {
bevy_error!("One arg");
bevy_error!(crate::error::Severity::Debug, "With severity");
bevy_error!(
crate::error::Severity::Debug,
"With severity and args {}",
4 / 3
);
bevy_error!("Format string {}", 1 + 2);
}
#[test]
fn bevy_bail_macro() {
fn t(f: impl Fn() -> Result<(), BevyError>) {
let val = f();
assert!(val.is_err(), "expected error got {:?}", val);
}
t(|| bail!("One arg"));
t(|| bail!(crate::error::Severity::Debug, "With severity"));
t(|| {
bail!(
crate::error::Severity::Debug,
"With severity and args {}",
2
)
});
t(|| bail!("Format string {}", 1 + 2));
}
#[test]
fn bevy_ensure_macro() {
fn validate(value: i32) -> Result<(), BevyError> {
ensure!(value != 0, "value must not be zero");
ensure!(
value > 0,
crate::error::Severity::Warning,
"value must be positive: {}",
value
);
Ok(())
}
assert!(validate(1).is_ok());
let zero = validate(0).unwrap_err();
assert_eq!(zero.severity(), crate::error::Severity::Panic);
assert!(zero.to_string().starts_with("value must not be zero"));
let negative = validate(-1).unwrap_err();
assert_eq!(negative.severity(), crate::error::Severity::Warning);
assert!(negative
.to_string()
.starts_with("value must be positive: -1"));
}
#[test]
fn context() {
let empty = None::<i32>;
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<i32, BevyError> =
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!
"
));
}
#[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<i32, Fun> = Err(Fun(1));
let new_error = fun.context("Hello world!");
assert!(new_error.as_ref().unwrap_err().is::<Fun>());
assert_eq!(
new_error.as_ref().unwrap_err().downcast_ref::<Fun>(),
Some(&Fun(1))
);
let new_new_error = new_error.context("Hey there!");
assert!(new_new_error.as_ref().unwrap_err().is::<Fun>());
assert_eq!(
new_new_error.as_ref().unwrap_err().downcast_ref::<Fun>(),
Some(&Fun(1))
);
}
}