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

  1. 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.
  2. Delete one edge of the reported cycle - usually the most recently added constraint or the one duplicated by .chain().
  3. 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

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


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/101ff2b98e29f885. Report an issue: GitHub.