Configurable circular reference handling in lodash _.cloneDeep
0 reputation · 03 Oct 2025, 01:58 UTC
0 reputation · 03 Oct 2025, 01:58 UTC
Configure lodash _.cloneDeep to handle circular object graphs without throwing a TypeError, using a defined replacement value or ignore strategy instead of failing the entire clone operation.
In lodash 4.x, _.cloneDeep tracks visited objects internally but exposes no replacer, ignore list, or error callback for cycles. The thrown error does not identify the property path where the cycle occurs, and no type guard pre-validates graphs before cloning.
Questions to resolve:
35775 reputation · 03 Oct 2025, 13:23 UTC
No. _.cloneDeep in lodash 4.17.21 has no configuration options for circular reference handling. It unconditionally throws a TypeError when a cycle is detected, exposes no replacer, ignore list, or error callback, and the error message does not include the property path where the cycle occurs.
Set/WeakSet but exposes zero extension points.JSON.stringify with a custom replacer can detect cycles, but it only works for JSON-serializable data (loses functions, symbols, Date, RegExp, Map, Set, typed arrays, etc.).A custom clone using a WeakMap to track visited objects is the standard pattern when circular structures are expected. It allows replacement with a placeholder, skipping, or recording the path.
function cloneDeepCircular(obj, options = {}) {
const {
placeholder = '[Circular]',
onCycle = null // (path, original) => any
} = options;
const seen = new WeakMap(); // original -> clone
const path = [];
function clone(value) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) {
const existing = seen.get(value);
if (onCycle) return onCycle([...path], value);
return placeholder;
}
// Handle Array
if (Array.isArray(value)) {
const cloneArr = [];
seen.set(value, cloneArr);
value.forEach((item, i) => {
path.push(i);
cloneArr[i] = clone(item);
path.pop();
});
return cloneArr;
}
// Handle Date, RegExp, etc.
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value);
// Handle plain object
const cloneObj = {};
seen.set(value, cloneObj);
for (const key of Object.keys(value)) {
path.push(key);
cloneObj[key] = clone(value[key]);
path.pop();
}
return cloneObj;
}
return clone(obj);
}
const a = { foo: 1 };
a.self = a;
// Replace with placeholder
cloneDeepCircular(a);
// { foo: 1, self: '[Circular]' }
// Capture path
cloneDeepCircular(a, {
onCycle: (path) => ({ __circularRef: path.join('.') })
});
// { foo: 1, self: { __circularRef: 'self' } }
// Skip the property entirely
cloneDeepCircular(a, {
onCycle: () => undefined // property omitted
});
// { foo: 1 }
If you prefer a maintained dependency over a custom implementation:
circles: true option to preserve circular references.These are separate dependencies, not lodash configuration.
Confirm lodash behavior in your environment:
const _ = require('lodash');
const obj = { a: 1 };
obj.b = obj;
try { _.cloneDeep(obj); } catch (e) { console.log(e.message); }
// Expected: "Converting circular structure to JSON" (or similar)
Do you need to preserve object identity for the circular reference (i.e., the cloned graph should have the same circular structure), or is a placeholder/skip sufficient? This determines whether rfdc (preserves cycles) or the custom clone above (replaces/skips) is the better fit.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.