How Lodash mergeWith Prevents Infinite Recursion
In the Lodash library, _.mergeWith safely handles
circularly referenced objects by delegating operations to an internal
traversal function that maintains an operational state stack. Instead of
blindly traversing object trees, Lodash uses an internal caching
mechanism to track object references encountered during the recursive
walk. When an already-visited object is encountered within the source
hierarchy, Lodash resolves the reference from the stack rather than
initiating a new recursive call, completely mitigating the risk of
RangeError: Maximum call stack size exceeded errors and
denial-of-service vulnerabilities.
At the core of this safety mechanism is Lodash's internal
baseMerge function, which accepts an optional hidden
argument: stack. When _.mergeWith or
_.merge is invoked, Lodash initializes an instance of its
internal Stack class if one has not already been provided.
This stack object acts as an associative lookup structure that persists
across all nested invocations during that single merge operation.
As the merge algorithm descends into nested objects or arrays via
baseMergeDeep, it executes the following sequence:
- Pre-Traversal Lookup: Before recursing into a
non-primitive value, the algorithm checks
stack.get(srcValue). If this source reference already exists in the stack, Lodash immediately assigns the retrieved cached target reference to the destination key and halts further recursion along that branch. - Stack Registration: If the object has not been
visited, Lodash creates or determines the corresponding target structure
(array or plain object), then calls
stack.set(srcValue, assigned). This step pairs the original source reference with the newly generated or modified destination reference before iterating over its keys. - Recursive Descent: The function iterates over the
object’s enumerable properties, passing the customizer callback (if
supplied to
_.mergeWith) and the ongoingstackto subsequent recursive calls. - Reference Preservation: If an inner property points back to an ancestor object, the pre-traversal check detects the ancestor in the stack, returns the matching assigned target, and exits the branch safely.
Lodash’s Stack implementation is optimized for both
performance and memory efficiency. For small numbers of keys (fewer than
200 elements), it uses a lightweight array of key-value pairs called
ListCache. When the number of tracked objects exceeds this
threshold, it automatically upgrades itself to a MapCache,
which utilizes JavaScript’s native Map whenever
available.
Because the stack exists only for the duration of the top-level merge
execution, all tracked references become eligible for garbage collection
once _.mergeWith returns its final output, avoiding
long-term memory leaks while maintaining robust protection against
circular recursion.