Detecting Cycles in Graphs
Learn how to detect cycles in directed graphs using DFS, in-progress nodes, and done states, with dependency graph examples.
A cycle is a path that comes back to where it started:
A -> B -> C -> A
That shape is not automatically bad.
In a map, a cycle is ordinary. You can drive around a block.
In a social network, a cycle is ordinary. People can know each other in loops.
In a package dependency graph, a cycle can break the build:
ui-kit -> shared-utils -> ui-kit
Now neither package has a clean place to start.
The graph shape is the same. The meaning is different.
That is why cycle detection is not just an algorithm trick. It is a model check.
Why A Visited Set Is Not Enough
For directed cycle detection, a plain visited set is too blunt.
Imagine this graph:
A -> B -> C
A ------> C
C is reached twice.
That is not a cycle.
It just means two paths share a dependency.
Now compare:
A -> B
^ |
|____|
Here the path loops back into itself.
That is a cycle.
To tell the difference, DFS needs three states:
type NodeState = "unvisited" | "in-progress" | "done";
The states mean:
unvisited = never explored
in-progress = currently on the active path
done = fully explored
Seeing a done node again is fine.
Seeing an in-progress node again means the current path has looped back into itself.
The Code
type NodeState = "unvisited" | "in-progress" | "done";
function hasCycle(graph: Map<string, string[]>): boolean {
const state = new Map<string, NodeState>();
for (const node of graph.keys()) {
state.set(node, "unvisited");
}
function visit(node: string): boolean {
state.set(node, "in-progress");
for (const neighbor of graph.get(node) ?? []) {
if (state.get(neighbor) === "in-progress") return true;
if (state.get(neighbor) === "unvisited" && visit(neighbor)) {
return true;
}
}
state.set(node, "done");
return false;
}
for (const node of graph.keys()) {
if (state.get(node) === "unvisited" && visit(node)) {
return true;
}
}
return false;
}
The important idea is not the code shape.
The important idea is the active path.
A directed cycle exists when a path reaches something already on that same path.
What To Do With A Cycle
Detection is only the first step.
Once you find a cycle, ask what it means:
In a map:
probably fine
In a workflow:
maybe intentional retry behavior
In a dependency graph:
probably a design smell
In a game state graph:
expected, because positions can repeat
Algorithms can find structure.
The model tells you whether that structure is healthy.