bevyengine/bevy · error · DiGraphToposortError
cycles detected: {0:?}
Error message
cycles detected: {0:?} What it means
The schedule dependency graph is topologically sorted before execution; this variant (DiGraphToposortError::Cycle) reports one or more cycles of length >= 2: two or more systems/sets order against each other in a loop (A before B while B before A), so no valid run order exists. Each inner Vec in the payload is one cycle, listed by node ids.
Source
Thrown at crates/bevy_ecs/src/schedule/graph/graph_map.rs:526
}
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`.
#[inline]
pub fn opposite(self) -> Self {
match self {View on GitHub (pinned to 396ca72708)
Solutions
- Trigger the failure deterministically (schedule.initialize(&mut world) or one app.update() in a test) and print the error with e.to_string(schedule.graph(), world) - it lists each cycle and its nodes.
- Delete one edge of the reported cycle - usually the most recently added constraint or the one duplicated by .chain().
- Centralize ordering in configure_sets so the relative order between feature sets is declared in exactly one place.
Example fix
// before app.add_systems(Update, (a, b).chain()); // implies a -> b app.add_systems(Update, a.after(b)); // implies b -> a: cycle // after app.add_systems(Update, (a, b).chain());
Defensive patterns
Strategy: validation
Validate before calling
// Catch ordering cycles at build time, not mid-game
#[test]
fn schedule_has_no_cycles() {
let mut app = App::new();
// add all plugins/systems/sets the app composes
app.update();
} Try / catch
match schedule.initialize(&mut world) {
Ok(()) => schedule.run(&mut world),
Err(e) => log::error!("cycles: {}", e.to_string(schedule.graph(), world)),
} Prevention
- Declare cross-plugin ordering once at the set level (feature sets before/after each other).
- When adding .chain() to existing tuples, audit the manual .before()/.after() calls you wrote for those systems.
- Add a startup smoke test per schedule configuration.
When it happens
Trigger: Conflicting constraints such as a.before(b) combined with b.after(a); mixing tuple .chain() (which implies a->b) with an explicit constraint in the opposite direction; two sets configured to run before each other; a system ordered against a set whose members are ordered back against the first system.
Common situations: Ordering constraints split across multiple plugins that each contribute half of a loop; renaming or moving systems so an old constraint now closes a cycle; adding .chain() to an existing tuple that already had manual .before()/.after() calls.
Related errors
- self-loop detected at node `{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/101ff2b98e29f885.
Report an issue: GitHub.