docs/bevy/examples/ecs/error_handling.rs 6.7 K · 198 lines · raw · history

1 //! Showcases how fallible systems and observers can make use of Rust's powerful result handling
2 //! syntax.
3
4 use bevy::ecs::{entity::SpawnError, error::warn, world::DeferredWorld};
5 use bevy::prelude::*;
6 use bevy::shape::sampling::UniformMeshSampler;
7
8 use chacha20::ChaCha8Rng;
9 use rand::distr::Distribution;
10 use rand::SeedableRng;
11
12 fn main() {
13 let mut app = App::new();
14 // By default, fallible systems that return an error will respond according to the `Severity`` in the error.
15 // These will typically panic, unless `with_severity` is used to change the severity of the error.
16 //
17 // We can change this by configuring the fallback error handler, which applies to the entire app
18 // (you can also set it for specific `World`s).
19 // Here we are using one of the built-in error handlers.
20 // Bevy provides built-in handlers for `panic`, `error`, `warn`, `info`,
21 // `debug`, `trace` and `ignore`.
22 app.set_error_handler(warn);
23
24 app.add_plugins(DefaultPlugins);
25
26 #[cfg(feature = "mesh_picking")]
27 app.add_plugins(MeshPickingPlugin);
28
29 // Fallible systems can be used the same way as regular systems. The only difference is they
30 // return a `Result<(), BevyError>` instead of a `()` (unit) type. Bevy will handle both
31 // types of systems the same way, except for the error handling.
32 app.add_systems(Startup, setup);
33
34 // Commands can also return `Result`s, which are automatically handled by the fallback error handler
35 // if not explicitly handled by the user.
36 app.add_systems(Startup, failing_commands);
37
38 // Individual systems can also be handled by piping the output result:
39 app.add_systems(
40 PostStartup,
41 failing_system.pipe(|result: In<Result>| {
42 let _ = result.0.inspect_err(|err| info!("captured error: {err}"));
43 }),
44 );
45
46 // Fallible observers are also supported.
47 app.add_observer(fallible_observer);
48
49 // If we run the app, we'll see the following output at startup:
50 //
51 // WARN Encountered an error in system `fallible_systems::failing_system`: Resource not initialized
52 // ERROR fallible_systems::failing_system failed: Resource not initialized
53 // INFO captured error: Resource not initialized
54 app.run();
55 }
56
57 /// An example of a system that calls several fallible functions with the question mark operator.
58 ///
59 /// See: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
60 fn setup(
61 mut commands: Commands,
62 mut meshes: ResMut<Assets<Mesh>>,
63 mut materials: ResMut<Assets<StandardMaterial>>,
64 ) -> Result {
65 let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
66
67 // Make a plane for establishing space.
68 commands.spawn((
69 Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))),
70 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
71 Transform::from_xyz(0.0, -2.5, 0.0),
72 ));
73
74 // Spawn a light:
75 commands.spawn((
76 PointLight {
77 shadow_maps_enabled: true,
78 ..default()
79 },
80 Transform::from_xyz(4.0, 8.0, 4.0),
81 ));
82
83 // Spawn a camera:
84 commands.spawn((
85 Camera3d::default(),
86 Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
87 ));
88
89 // Create a new sphere mesh:
90 let mut sphere_mesh = Sphere::new(1.0).mesh().ico(7)?;
91 sphere_mesh.generate_tangents()?;
92
93 // Spawn the mesh into the scene:
94 let mut sphere = commands.spawn((
95 Mesh3d(meshes.add(sphere_mesh.clone())),
96 MeshMaterial3d(materials.add(StandardMaterial::default())),
97 Transform::from_xyz(-1.0, 1.0, 0.0),
98 ));
99
100 // Generate random sample points:
101 let triangles = sphere_mesh.triangles()?;
102 let distribution = UniformMeshSampler::try_new(triangles)?;
103
104 // Setup sample points:
105 let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?);
106 let point_material = materials.add(StandardMaterial {
107 base_color: Srgba::RED.into(),
108 emissive: LinearRgba::rgb(1.0, 0.0, 0.0),
109 ..default()
110 });
111
112 // Add sample points as children of the sphere:
113 for point in distribution.sample_iter(&mut seeded_rng).take(10000) {
114 sphere.with_child((
115 Mesh3d(point_mesh.clone()),
116 MeshMaterial3d(point_material.clone()),
117 Transform::from_translation(point),
118 ));
119 }
120
121 // Indicate the system completed successfully:
122 Ok(())
123 }
124
125 // Observer systems can also return a `Result`.
126 fn fallible_observer(
127 pointer_move: On<PointerMove>,
128 mut world: DeferredWorld,
129 mut step: Local<f32>,
130 ) -> Result {
131 let mut transform = world
132 .get_mut::<Transform>(pointer_move.entity)
133 .ok_or("No transform found.")?;
134
135 *step = if transform.translation.x > 3. {
136 -0.1
137 } else if transform.translation.x < -3. || *step == 0. {
138 0.1
139 } else {
140 *step
141 };
142
143 transform.translation.x += *step;
144
145 Ok(())
146 }
147
148 #[derive(Resource)]
149 struct UninitializedResource;
150
151 fn failing_system(world: &mut World) -> Result {
152 world
153 // `get_resource` returns an `Option<T>`, so we use `ok_or` to convert it to a `Result` on
154 // which we can call `?` to propagate the error.
155 .get_resource::<UninitializedResource>()
156 // We can provide a `str` here because `BevyError` implements `From<&str>`.
157 .ok_or("Resource not initialized")
158 // The default error severity is Severity::Panic.
159 // We can add a Severity level to any Result locally to downgrade it appropriately.
160 .with_severity(Severity::Warning)?;
161
162 world
163 // This entity doesn't exist!
164 .spawn_empty_at(Entity::from_raw_u32(12345678).unwrap())
165 .map_severity(|e| match e {
166 // Not that concerning, we just need to make sure to find a different entity
167 SpawnError::AlreadySpawned => Severity::Debug,
168 // Oh no
169 SpawnError::Invalid(_) => Severity::Error,
170 })?;
171
172 Ok(())
173 }
174
175 fn failing_commands(mut commands: Commands) {
176 commands
177 // This entity doesn't exist!
178 .entity(Entity::from_raw_u32(12345678).unwrap())
179 // Normally, this failed command would panic,
180 // but since we've set the fallback error handler to `warn`
181 // it will log a warning instead.
182 .insert(Transform::default());
183
184 // The error handlers for commands can be set individually as well,
185 // by using the queue_handled method.
186 commands.queue_handled(
187 |world: &mut World| -> Result {
188 world
189 .get_resource::<UninitializedResource>()
190 .ok_or("Resource not initialized when accessed in a command")?;
191
192 Ok(())
193 },
194 |error, context| {
195 error!("{error}, {context}");
196 },
197 );
198 }