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:

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 JSON

This error applies to:

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:

  1. _.isEqual maintains state stacks (stackA and stackB) tracking every parent node visited along the current traversal path.
  2. Before inspecting an object's nested properties, Lodash checks whether the current pair of objects has already been compared higher in the traversal tree.
  3. If an existing recursive cycle is detected between the two matching object nodes, _.isEqual resolves that specific recursive step as equal (true) rather than descending further.
  4. 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 crashing

Direct 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.