
## Objective Fixes #18092 Bevy's current error type is a simple type alias for `Box<dyn Error + Send + Sync + 'static>`. This largely works as a catch-all error, but it is missing a critical feature: the ability to capture a backtrace at the point that the error occurs. The best way to do this is `anyhow`-style error handling: a new error type that takes advantage of the fact that the `?` `From` conversion happens "inline" to capture the backtrace at the point of the error. ## Solution This PR adds a new `BevyError` type (replacing our old `std::error::Error` type alias), which uses the "from conversion backtrace capture" approach: ```rust fn oh_no() -> Result<(), BevyError> { // this fails with Rust's built in ParseIntError, which // is converted into the catch-all BevyError type let number: usize = "hi".parse()?; println!("parsed {number}"); Ok(()) } ``` This also updates our exported `Result` type alias to default to `BevyError`, meaning you can write this instead: ```rust fn oh_no() -> Result { let number: usize = "hi".parse()?; println!("parsed {number}"); Ok(()) } ``` When a BevyError is encountered in a system, it will use Bevy's default system error handler (which panics by default). BevyError does custom "backtrace filtering" by default, meaning we can cut out the _massive_ amount of "rust internals", "async executor internals", and "bevy system scheduler internals" that show up in backtraces. It also trims out the first generally-unnecssary `From` conversion backtrace lines that make it harder to locate the real error location. The result is a blissfully simple backtrace by default:  The full backtrace can be shown by setting the `BEVY_BACKTRACE=full` environment variable. Non-BevyError panics still use the default Rust backtrace behavior. One issue that prevented the truly noise-free backtrace during panics that you see above is that Rust's default panic handler will print the unfiltered (and largely unhelpful real-panic-point) backtrace by default, in _addition_ to our filtered BevyError backtrace (with the helpful backtrace origin) that we capture and print. To resolve this, I have extended Bevy's existing PanicHandlerPlugin to wrap the default panic handler. If we panic from the result of a BevyError, we will skip the default "print full backtrace" panic handler. This behavior can be enabled and disabled using the new `error_panic_hook` cargo feature in `bevy_app` (which is enabled by default). One downside to _not_ using `Box<dyn Error>` directly is that we can no longer take advantage of the built-in `Into` impl for strings to errors. To resolve this, I have added the following: ```rust // Before Err("some error")? // After Err(BevyError::message("some error"))? ``` We can discuss adding shorthand methods or macros for this (similar to anyhow's `anyhow!("some error")` macro), but I'd prefer to discuss that later. I have also added the following extension method: ```rust // Before some_option.ok_or("some error")?; // After some_option.ok_or_message("some error")?; ``` I've also moved all of our existing error infrastructure from `bevy_ecs::result` to `bevy_ecs::error`, as I think that is the better home for it ## Why not anyhow (or eyre)? The biggest reason is that `anyhow` needs to be a "generically useful error type", whereas Bevy is a much narrower scope. By using our own error, we can be significantly more opinionated. For example, anyhow doesn't do the extensive (and invasive) backtrace filtering that BevyError does because it can't operate on Bevy-specific context, and needs to be generically useful. Bevy also has a lot of operational context (ex: system info) that could be useful to attach to errors. If we have control over the error type, we can add whatever context we want to in a structured way. This could be increasingly useful as we add more visual / interactive error handling tools and editor integrations. Additionally, the core approach used is simple and requires almost no code. anyhow clocks in at ~2500 lines of code, but the impl here uses 160. We are able to boil this down to exactly what we need, and by doing so we improve our compile times and the understandability of our code.
169 lines
5.5 KiB
Rust
169 lines
5.5 KiB
Rust
//! Showcases how fallible systems and observers can make use of Rust's powerful result handling
|
|
//! syntax.
|
|
|
|
use bevy::ecs::world::DeferredWorld;
|
|
use bevy::math::sampling::UniformMeshSampler;
|
|
use bevy::prelude::*;
|
|
|
|
use rand::distributions::Distribution;
|
|
use rand::SeedableRng;
|
|
use rand_chacha::ChaCha8Rng;
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
|
|
app.add_plugins(DefaultPlugins);
|
|
|
|
#[cfg(feature = "bevy_mesh_picking_backend")]
|
|
app.add_plugins(MeshPickingPlugin);
|
|
|
|
// Fallible systems can be used the same way as regular systems. The only difference is they
|
|
// return a `Result<(), BevyError>` instead of a `()` (unit) type. Bevy will handle both
|
|
// types of systems the same way, except for the error handling.
|
|
app.add_systems(Startup, setup);
|
|
|
|
// By default, fallible systems that return an error will panic.
|
|
//
|
|
// We can change this by setting a custom error handler. This can be done globally for all
|
|
// systems in a given `App`. Here we set the global error handler using one of the built-in
|
|
// error handlers. Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
|
|
// `debug`, `trace` and `ignore`.
|
|
app.set_system_error_handler(bevy::ecs::error::warn);
|
|
|
|
// Additionally, you can set a custom error handler per `Schedule`. This will take precedence
|
|
// over the global error handler.
|
|
//
|
|
// In this instance we provide our own non-capturing closure that coerces to the expected error
|
|
// handler function pointer:
|
|
//
|
|
// fn(bevy_ecs::error::BevyError, bevy_ecs::error::SystemErrorContext)
|
|
//
|
|
app.add_systems(PostStartup, failing_system)
|
|
.get_schedule_mut(PostStartup)
|
|
.unwrap()
|
|
.set_error_handler(|err, ctx| error!("{} failed: {err}", ctx.name));
|
|
|
|
// Individual systems can also be handled by piping the output result:
|
|
app.add_systems(
|
|
PostStartup,
|
|
failing_system.pipe(|result: In<Result>| {
|
|
let _ = result.0.inspect_err(|err| info!("captured error: {err}"));
|
|
}),
|
|
);
|
|
|
|
// Fallible observers are also supported.
|
|
app.add_observer(fallible_observer);
|
|
|
|
// If we run the app, we'll see the following output at startup:
|
|
//
|
|
// WARN Encountered an error in system `fallible_systems::failing_system`: "Resource not initialized"
|
|
// ERROR fallible_systems::failing_system failed: Resource not initialized
|
|
// INFO captured error: Resource not initialized
|
|
app.run();
|
|
}
|
|
|
|
/// An example of a system that calls several fallible functions with the question mark operator.
|
|
///
|
|
/// See: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) -> Result {
|
|
let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
|
|
|
|
// Make a plane for establishing space.
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
|
|
Transform::from_xyz(0.0, -2.5, 0.0),
|
|
));
|
|
|
|
// Spawn a light:
|
|
commands.spawn((
|
|
PointLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
|
|
// Spawn a camera:
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
|
|
// Create a new sphere mesh:
|
|
let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?;
|
|
sphere_mesh.generate_tangents()?;
|
|
|
|
// Spawn the mesh into the scene:
|
|
let mut sphere = commands.spawn((
|
|
Mesh3d(meshes.add(sphere_mesh.clone())),
|
|
MeshMaterial3d(materials.add(StandardMaterial::default())),
|
|
Transform::from_xyz(-1.0, 1.0, 0.0),
|
|
));
|
|
|
|
// Generate random sample points:
|
|
let triangles = sphere_mesh.triangles()?;
|
|
let distribution = UniformMeshSampler::try_new(triangles)?;
|
|
|
|
// Setup sample points:
|
|
let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?);
|
|
let point_material = materials.add(StandardMaterial {
|
|
base_color: Srgba::RED.into(),
|
|
emissive: LinearRgba::rgb(1.0, 0.0, 0.0),
|
|
..default()
|
|
});
|
|
|
|
// Add sample points as children of the sphere:
|
|
for point in distribution.sample_iter(&mut seeded_rng).take(10000) {
|
|
sphere.with_child((
|
|
Mesh3d(point_mesh.clone()),
|
|
MeshMaterial3d(point_material.clone()),
|
|
Transform::from_translation(point),
|
|
));
|
|
}
|
|
|
|
// Indicate the system completed successfully:
|
|
Ok(())
|
|
}
|
|
|
|
// Observer systems can also return a `Result`.
|
|
fn fallible_observer(
|
|
trigger: Trigger<Pointer<Move>>,
|
|
mut world: DeferredWorld,
|
|
mut step: Local<f32>,
|
|
) -> Result {
|
|
let mut transform = world
|
|
.get_mut::<Transform>(trigger.target)
|
|
.ok_or("No transform found.")?;
|
|
|
|
*step = if transform.translation.x > 3. {
|
|
-0.1
|
|
} else if transform.translation.x < -3. || *step == 0. {
|
|
0.1
|
|
} else {
|
|
*step
|
|
};
|
|
|
|
transform.translation.x += *step;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct UninitializedResource;
|
|
|
|
fn failing_system(world: &mut World) -> Result {
|
|
world
|
|
// `get_resource` returns an `Option<T>`, so we use `ok_or` to convert it to a `Result` on
|
|
// which we can call `?` to propagate the error.
|
|
.get_resource::<UninitializedResource>()
|
|
// We can provide a `str` here because `BevyError` implements `From<&str>`.
|
|
.ok_or("Resource not initialized")?;
|
|
|
|
Ok(())
|
|
}
|