bevyengine/bevy · error · DiGraphToposortError
self-loop detected at node `{0:?}`
Error message
self-loop detected at node `{0:?}` What it means
Bevy's schedule builder turns ordering constraints (.before/.after/.chain) and set membership into edges of a directed graph, then topologically sorts it to pick a run order. This error (DiGraphToposortError::Loop) means a single node - a system or system set - has an edge pointing directly at itself, so no valid ordering exists. It is produced while the schedule graph is topologically sorted during schedule initialization.
Source
Thrown at crates/bevy_ecs/src/schedule/graph/graph_map.rs:523
// divide remainder into smaller SCCs
sccs.extend(subgraph.iter_sccs().filter(|scc| scc.len() > 1));
}
cycles
}
/// Iterate over all *Strongly Connected Components* in this graph.
pub(crate) fn iter_sccs(&self) -> impl Iterator<Item = SmallVec<[N; 4]>> + '_ {
super::tarjan_scc::new_tarjan_scc(self)
}
}
/// Error returned when topologically sorting a directed graph fails.
#[derive(Error, Debug)]
pub enum DiGraphToposortError<N: GraphNodeId> {
/// A self-loop was detected.
#[error("self-loop detected at node `{0:?}`")]
Loop(N),
/// Cycles were detected.
#[error("cycles detected: {0:?}")]
Cycle(Vec<Vec<N>>),
}
/// Edge direction.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)]
#[repr(u8)]
pub enum Direction {
/// An `Outgoing` edge is an outward edge *from* the current node.
Outgoing = 0,
/// An `Incoming` edge is an inbound edge *to* the current node.
Incoming = 1,
}
impl Direction {
/// Return the opposite `Direction`.View on GitHub (pinned to 396ca72708)
Solutions
- Map the node id printed in the error back to the system/set it names, then inspect its .before()/.after()/.in_set() constraints and delete the one that points the node at itself.
- If two different systems were intended, order against a distinct function or wrap the target in a named SystemSet and order against that set.
- Add a startup test that calls app.update() once (or schedule.initialize(&mut world)) so graph errors fail in CI instead of at first frame.
Example fix
// before app.configure_sets(Update, Physics.in_set(Physics)); // set is a member of itself -> self-loop // after app.configure_sets(Update, Physics);
Defensive patterns
Strategy: validation
Validate before calling
// Validate the schedule graph at startup instead of first run
#[test]
fn schedule_graph_is_valid() {
let mut app = App::new();
// register systems/sets exactly as the real app does
app.update(); // panics (with the offending node named) if a self-loop exists
} Try / catch
if let Err(e) = schedule.initialize(&mut world) {
// formatted against the graph: names the self-looped node
log::error!("schedule build failed: {}", e.to_string(schedule.graph(), world));
} Prevention
- Prefer tuple .chain() over hand-written .before()/.after() - it cannot self-reference.
- Keep ordering constraints in one place (configure_sets) instead of repeating them per system.
- Run one app.update() smoke test per plugin so graph errors surface in CI.
When it happens
Trigger: An ordering or membership constraint that resolves to the node itself: system fn ordered against itself (move_player.before(move_player) resolves the function name to that same system node), a set configured into itself (Physics.in_set(Physics)), or generated/config-driven code that emits a constraint whose source and target are the same node.
Common situations: Copy-pasted ordering constraints where the target name was not updated; sets accidentally nested into themselves during refactors; macro-generated plugin code that applies a user-supplied ordering twice onto the same node; Bevy upgrades where trivially self-referential ordering stopped being silently ignored.
Related errors
- cycles detected: {0:?}
- HierarchySort
- DependencySort
- FlatDependencySort
- Systems with conflicting access have indeterminate run order
AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20).
Data as JSON: /api/errors/15cfa4acb763233d.
Report an issue: GitHub.