nodejs/node · error · DependencyGraphNode.CircularException

Cycles in dependency graph detected: {cycles}

Error message

Cycles in dependency graph detected:
{cycles}

What it means

After building target nodes, gyp flattens the graph from a synthetic root_node. If the flattened list length != number of targets, some nodes are mutually reachable only through a cycle. gyp then runs FindCycles and raises DependencyGraphNode.CircularException listing each cycle path as TargetA -> TargetB -> ... -> TargetA.

Source

Thrown at tools/gyp/pylib/gyp/input.py:1974

    flat_list = root_node.FlattenToList()

    # If there's anything left unvisited, there must be a circular dependency
    # (cycle).
    if len(flat_list) != len(targets):
        if not root_node.dependents:
            # If all targets have dependencies, add the first target as a dependent
            # of root_node so that the cycle can be discovered from root_node.
            target = next(iter(targets))
            target_node = dependency_nodes[target]
            target_node.dependencies.append(root_node)
            root_node.dependents.append(target_node)

        cycles = []
        for cycle in root_node.FindCycles():
            paths = [node.ref for node in cycle]
            cycles.append("Cycle: %s" % " -> ".join(paths))
        raise DependencyGraphNode.CircularException(
            "Cycles in dependency graph detected:\n" + "\n".join(cycles)
        )

    return [dependency_nodes, flat_list]


def VerifyNoGYPFileCircularDependencies(targets):
    # Create a DependencyGraphNode for each gyp file containing a target.  Put
    # it into a dict for easy access.
    dependency_nodes = {}
    for target in targets:
        build_file = gyp.common.BuildFile(target)
        if build_file not in dependency_nodes:
            dependency_nodes[build_file] = DependencyGraphNode(build_file)

    # Set up the dependency links.
    for target, spec in targets.items():
        build_file = gyp.common.BuildFile(target)

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Identify the cycle in the reported path and remove or redirect one edge to break the ring.
  2. Move shared code into a third target that both cyclic members depend on instead of each other.
  3. Use direct_dependent_settings/all_dependent_settings to share settings without adding a hard dependency edge.
  4. Re-run after each edge change to confirm FlattenToList succeeds.

Example fix

// before: a.gyp:libA depends on libB, b.gyp:libB depends on libA
// break the ring by extracting common code
'dependencies': ['common.gyp:common']
Defensive patterns

Strategy: validation

Validate before calling

def has_cycle(targets):
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {t: WHITE for t in targets}
    def dfs(n):
        color[n] = GRAY
        for d in targets[n].get('dependencies', []):
            if d not in color: continue
            if color[d] == GRAY: return True
            if color[d] == WHITE and dfs(d): return True
        color[n] = BLACK
        return False
    return any(color[t] == WHITE and dfs(t) for t in targets)

Try / catch

try:
    gyp.main(args)
except gyp.input.DependencyGraphNode.CircularException as e:
    print('Dependency cycle:', e); raise

Prevention

When it happens

Trigger: A set of targets form a dependency cycle (A depends on B, B depends on A, or longer rings). The length-mismatch check plus FindCycles enumerates and reports them. If no node is a root dependent, the first target is artificially attached to root_node so the cycle becomes discoverable.

Common situations: Two libraries mutually depending; a target accidentally listing a dep that transitively points back; refactoring that moved a dependency from a child to a parent creating a ring; conditional dependencies that only cycle in certain configs.

Related errors


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