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

  1. Inspect the node set in the CycleError to identify the cycle, then break one edge in the offending .gyp/.gypi.
  2. For variable cycles, audit $(...) macro references and remove the self/loop reference.
  3. Use gyp --debug general or dump the graph to visualize dependencies before sorting.
  4. 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

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


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/169630007b5ed659. Report an issue: GitHub.