2026-06-30
The asker is writing a Bevy 0.18 Observer that reacts to the removal of a public marker component (Animated) and tears down an internal bookkeeping component (TimedBy). The observer works fine when Animated is explicitly removed, but it also fires when the whole entity is despawned. At that point the deferred commands.entity(event.entity).remove::<TimedBy>() runs against an entity that no longer exists, producing a warning.
What makes this interesting is that it sits on a subtle ECS lifecycle distinction. In Bevy, despawn is implemented as "remove every component, then deallocate the entity." From the observer's perspective, a despawn is indistinguishable from a targeted remove::<Animated>() — both produce a Remove event with the same entity. The order is what bites you: the observer runs synchronously when the component is removed, but the work it schedules via Commands is deferred until the next command flush. By then, the entity is gone.
Approach 1 — check existence at command time. Wrap the deferred work so it no-ops if the entity is already gone:
fn on_remove_animated(event: On<Remove, Animated>, mut commands: Commands) {
commands.queue(move |world: &mut World| {
if let Ok(mut e) = world.get_entity_mut(event.entity) {
e.remove::<TimedBy>();
}
});
}
This is the most robust pattern because it tolerates any race — including despawn from another system that fires between observer and flush.
Approach 2 — use the exclusive form via try_*. Bevy has been moving toward fallible entity command APIs (EntityCommands::try_remove in recent versions). If your version exposes it, swap .remove() for .try_remove() and the warning disappears without an explicit existence check.
Approach 3 — observe Despawn separately. Bevy 0.18 has a Despawn event you can hook with a second observer. But this doesn't solve the underlying problem: the Remove observer still fires during despawn, because despawn really does remove components. You'd need to gate inside the Remove handler on "is this entity also being despawned right now?" — and there's no clean synchronous signal for that, since the despawn command may not have flushed yet.
Gotchas:
world.get_entity() inside the observer itself — the entity is still alive at that point. The mismatch only appears at command-flush time.Remove, each will face the same race. Centralize the "is entity still alive" check in a small helper.Remove fires per-component during despawn in an undefined order, so don't assume TimedBy is still present when Animated's observer runs.Remove observer can't natively distinguish a targeted component removal from a despawn cascade, forcing you to defend against entity-gone-by-flush-time at the command layer.
