JSON.stringify vs Lodash isEqual Circular References
This article examines how native JSON.stringify and
Lodash’s _.isEqual handle deep, circular object graphs in
JavaScript. While both utilities perform deep recursive traversals
through object trees, JSON.stringify strictly prohibits
circular references by throwing a runtime TypeError,
whereas _.isEqual tracks object ancestry internally to
allow safe and accurate structural equality comparisons without
crashing.
The Mechanics of Circular References
A circular reference occurs when an object contains a reference to itself, either directly or indirectly through intermediate child properties:
- Direct self-reference:
const a = {}; a.self = a; - Mutual cross-reference:
const a = {}; const b = { ref: a }; a.ref = b; - Deep tree cycle: An arbitrarily nested descendant node references an ancestor node higher in the tree hierarchy.
Why JSON.stringify Crashes
The ECMAScript specification dictates that serialization must terminate cleanly into valid JSON text. Because the JSON standard (RFC 8259) does not define an addressable pointer mechanism (such as JSON Pointer or reference IDs), serializing an infinite recursive cycle is impossible.
When JSON.stringify runs, the native engine maintains a
stack of active ancestor objects. If an object is encountered that
already exists in the current traversal path, the engine halts execution
immediately and throws an unrecoverable exception:
const nodeA = { name: "Root" };
const nodeB = { name: "Child" };
nodeA.child = nodeB;
nodeB.parent = nodeA; // Deep circular reference
JSON.stringify(nodeA);
// Uncaught TypeError: Converting circular structure to JSONThis error applies to:
- Direct cyclical loops (
a -> a) - Multi-level nested cycles
(
a -> b -> c -> d -> a) - Objects containing arrays referencing parent structures
Unless a custom replacer function is supplied to strip or alter the cyclic references, any presence of an ancestor in its own branch will trigger a fatal crash.
How Lodash _.isEqual Handles Cycles
Lodash’s _.isEqual handles deep structural comparison
across objects without throwing errors, even when deep circular paths
are present.
Internally, Lodash employs a cycle-detection mechanism (historically
built using an internal Stack class leveraging
Map or parallel array storage). During recursion:
_.isEqualmaintains state stacks (stackAandstackB) tracking every parent node visited along the current traversal path.- Before inspecting an object's nested properties, Lodash checks whether the current pair of objects has already been compared higher in the traversal tree.
- If an existing recursive cycle is detected between the two matching
object nodes,
_.isEqualresolves that specific recursive step as equal (true) rather than descending further. - If one tree has a cycle and the other does not, or if the cyclic
target references a different node, the function resolves to
false.
const obj1 = { name: "Node" };
obj1.self = obj1;
const obj2 = { name: "Node" };
obj2.self = obj2;
_.isEqual(obj1, obj2); // Returns true without crashingDirect Comparison
| Feature | JSON.stringify |
Lodash _.isEqual |
|---|---|---|
| Direct Circular Reference | Throws TypeError |
Returns true or
false safely |
| Deep Tree Cycle | Throws TypeError |
Compares topology safely |
| Cycle Tracking Method | Native ancestor stack check | Dynamic internal Stack /
lookup cache |
| Purpose | Serialization to a tree format | Structural equality evaluation |
Native JSON.stringify fails on cyclic graphs by design
because valid JSON requires an acyclic tree structure. In contrast,
_.isEqual evaluates identity topologies, allowing it to
navigate arbitrary directed graphs without stack overflow or
exceptions.