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
- Identify the cycle in the reported path and remove or redirect one edge to break the ring.
- Move shared code into a third target that both cyclic members depend on instead of each other.
- Use direct_dependent_settings/all_dependent_settings to share settings without adding a hard dependency edge.
- 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
- Model dependencies as a DAG; never add a back-edge.
- Use settings propagation instead of hard deps when only flags are shared.
- Run cycle detection in CI on .gyp changes.
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
- Cycles in .gyp file dependency graph detected: {cycles}
- CycleError: cycle involving: %s
- Found wildcard in {dependency_key} of {target} referring to
- Dependency '%s' not found while trying to load target %s
- Dependency '%s' not found
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/91c29c7757c906a7.
Report an issue: GitHub.