What Is a Circular Structure?
A circular reference exists when an object has a property that points back to itself — directly or through a chain of other objects. JSON.stringify() cannot handle this and throws:
TypeError: Converting circular structure to JSONHow Circular References Are Created
Direct self-reference:
const obj = { name: 'Ravi' };
obj.self = obj; // obj.self points back to obj
JSON.stringify(obj); // TypeErrorParent-child cycle (common in DOM nodes, linked lists, trees with parent pointers):
const parent = { name: 'Parent', children: [] };
const child = { name: 'Child', parent };
parent.children.push(child);
// child.parent.children[0].parent.children[0]... infinite loop
JSON.stringify(parent); // TypeErrorNode.js objects — Error objects, Request objects, and many built-in Node.js objects have circular references internally:
const err = new Error('something failed');
JSON.stringify(err); // TypeError — Error has circular refsFix 1: Custom Replacer to Skip Circular Keys
function safeStringify(obj, indent) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
}, indent);
}
const result = safeStringify(obj, 2);
// Circular references become the string "[Circular]"Fix 2: Use the json-stringify-safe Package
npm install json-stringify-safeconst stringify = require('json-stringify-safe');
const json = stringify(obj, null, 2);Fix 3: Use flatted for Round-Trip Serialization
flatted encodes circular references in a way that can be decoded back:
npm install flattedimport { stringify, parse } from 'flatted';
const json = stringify(obj); // handles circular refs
const parsed = parse(json); // restores the structureThis is the best option when you need to serialize and deserialize the object while preserving the circular references.
Fix 4: Remove the Circular Reference Before Serializing
If you control the data structure, remove the back-reference before serializing:
const parent = { name: 'Parent', children: [] };
const child = { name: 'Child' }; // no parent pointer
parent.children.push(child);
JSON.stringify(parent); // works fineOr create a clean copy with only the fields you need:
const clean = {
id: user.id,
name: user.name,
email: user.email,
// omit: user.account (circular back-ref to user)
};
JSON.stringify(clean);Fix 5: Serializing Error Objects
Error objects do not serialize well by default — their properties are not enumerable:
const err = new Error('something failed');
JSON.stringify(err); // {} — empty object, or circular error
// Instead, build a plain object:
JSON.stringify({
message: err.message,
name: err.name,
stack: err.stack,
});Detecting Circular References Without Serializing
function hasCircular(obj, seen = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return false;
if (seen.has(obj)) return true;
seen.add(obj);
return Object.values(obj).some(v => hasCircular(v, seen));
}
console.log(hasCircular(obj)); // true or falseCommon Culprits in Node.js Projects
- Express req and res objects (huge circular ref graphs)
- Mongoose/Sequelize model instances (virtual properties, populated refs)
- DOM elements and React fiber objects
- Linked lists, trees with parent pointers
- Custom event emitters with back-references