nodejs/node · error · CycleError
CycleError: cycle involving: %s
Error message
CycleError: cycle involving: %s
What it means
TopologicallySorted (gyp/common.py) raises CycleError(visiting) when its depth-first Visit() re-enters a node currently on the recursion stack (the 'visiting' set). The argument is the set of nodes on the current path, so the message lists the nodes forming the cycle. Gyp uses this to order targets/variables; a cycle means a node (directly or transitively) depends on itself, so no valid build order exists.
Source
Thrown at tools/gyp/pylib/gyp/common.py:688
cheaper than repeatedly calling get_edges.
Raises:
CycleError in the event of a cycle.
Example:
graph = {'a': '$(b) $(c)', 'b': 'hi', 'c': '$(b)'}
def GetEdges(node):
return re.findall(r'\$\(([^))]\)', graph[node])
print TopologicallySorted(graph.keys(), GetEdges)
==>
['a', 'c', b']
"""
get_edges = memoize(get_edges)
visited = set()
visiting = set()
ordered_nodes = []
def Visit(node):
if node in visiting:
raise CycleError(visiting)
if node in visited:
return
visited.add(node)
visiting.add(node)
for neighbor in get_edges(node):
Visit(neighbor)
visiting.remove(node)
ordered_nodes.insert(0, node)
for node in sorted(graph):
Visit(node)
return ordered_nodes
def CrossCompileRequested():
# TODO: figure out how to not build extra host objects in the
# non-cross-compile case when this is enabled, and enable unconditionally.
return (View on GitHub (pinned to 1b2de5e052)
Solutions
- Inspect the node set in the CycleError to identify the cycle, then break one edge in the offending .gyp/.gypi.
- For variable cycles, audit $(...) macro references and remove the self/loop reference.
- Use gyp --debug general or dump the graph to visualize dependencies before sorting.
- Ensure get_edges terminates and returns only true forward edges.
Example fix
# before: A -> B -> A # target_a.gyp: 'dependencies': ['target_b.gyp:*'] # target_b.gyp: 'dependencies': ['target_a.gyp:*'] # after: break the back edge # target_b.gyp: (remove dependency on target_a)
Defensive patterns
Strategy: try-catch
Validate before calling
def is_acyclic(graph, get_edges):
visiting, visited = set(), set()
def visit(n):
if n in visiting: return False
if n in visited: return True
visiting.add(n)
for m in get_edges(n):
if not visit(m): return False
visiting.remove(n); visited.add(n)
return True
return all(visit(n) for n in graph) Type guard
def graph_is_acyclic(graph, get_edges) -> bool:
return is_acyclic(graph, get_edges) Try / catch
from gyp.common import TopologicallySorted, CycleError
try:
order = TopologicallySorted(graph, get_edges)
except CycleError as e:
# e.args[0] is the set of nodes on the cycle path
print('dependency cycle among:', e.args[0])
# break the cycle in the .gyp and retry Prevention
- Lint gyp dependency graphs for mutual references before building.
- Audit $(...) macro references for self/loop references when sorting variables.
- Keep target dependencies strictly layered (no back-edges).
When it happens
Trigger: A gyp dependency graph where target A depends on B and B on A; variable macros that reference each other ($($(A)) style) producing a self-referential edge function; a hand-written get_edges callback that returns an edge back to the start node.
Common situations: Refactoring targets and accidentally creating mutual dependencies; merging two .gypi includes that introduce a loop; bad macro expansion producing recursive variable references.
Related errors
- Found wildcard in {dependency_key} of {target} referring to
- Dependency '%s' not found while trying to load target %s
- Cycles in dependency graph detected: {cycles}
- Dependency '%s' not found
- Cycles in .gyp file dependency graph detected: {cycles}
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/169630007b5ed659.
Report an issue: GitHub.