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 serialize this because it would have to follow the reference forever, so it throws:
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'self' -> object with constructor 'Object'
--- property 'self' closes the circleModern Node.js (v12+) shows the full cycle path in the error message, which makes it easier to find.
How Circular References Are Created
Direct self-reference:
const obj = { name: "Ravi" };
obj.self = obj; // obj.self points back to obj itself
JSON.stringify(obj); // TypeErrorParent-child cycle (common in trees and linked lists):
const parent = { name: "Parent", children: [] };
const child = { name: "Child", parent }; // child holds a ref to parent
parent.children.push(child);
// Traversal: parent → children[0] → parent → children[0] → ...infinite
JSON.stringify(parent); // TypeErrorExpress/Koa request and response objects:
const express = require("express");
app.get("/debug", (req, res) => {
res.json(req); // TypeError — req has circular refs internally
});Node.js Error objects:
const err = new Error("failed");
JSON.stringify(err); // "{}" — empty! Error properties are non-enumerable
JSON.stringify({ message: err.message }); // works fineFix 1: Custom Replacer with WeakSet (Zero Dependencies)
The cleanest zero-dependency solution — replaces circular references with the string "[Circular]":
function safeStringify(obj, indent = 2) {
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);
}
// Usage
const obj = { name: "Ravi" };
obj.self = obj;
console.log(safeStringify(obj));
// {"name":"Ravi","self":"[Circular]"}This is safe to use in production logging, error reporting, and debugging.
Fix 2: The json-stringify-safe npm Package
Drop-in replacement for JSON.stringify that handles circular references:
npm install json-stringify-safeconst stringify = require("json-stringify-safe");
const json = stringify(circularObj, null, 2);
// Circular refs become "[Circular ~]" with path informationThis is the most battle-tested option for logging libraries and API serializers.
Fix 3: flatted for Round-Trip Serialization
flatted encodes circular references in a format that can be decoded back to the original structure — unlike the replacer approach which loses the circular ref permanently:
npm install flattedimport { stringify, parse } from "flatted";
const parent = { name: "Parent", children: [] };
const child = { name: "Child", parent };
parent.children.push(child);
const encoded = stringify(parent); // handles circular refs
const decoded = parse(encoded); // restores the full structure including refs
// decoded.children[0].parent === decoded (true — same reference)Use flatted when you need to serialize/deserialize complex object graphs (e.g., state management, undo history) without data loss.
Fix 4: Remove the Circular Reference Before Serializing
If you control the data structure, the cleanest fix is to design it without back-references:
// Instead of: child holds a reference to parent
const parent = { name: "Parent", children: [] };
const child = { name: "Child" }; // no parent pointer here
parent.children.push(child);
JSON.stringify(parent); // works fineOr project to a plain DTO (Data Transfer Object) that only contains the fields you want to serialize:
const dto = {
id: user.id,
name: user.name,
email: user.email,
// intentionally omits: user.account (would create a circular ref)
};
JSON.stringify(dto);Fix 5: Serializing Error Objects Correctly
Error objects have non-enumerable properties — JSON.stringify produces {}. Build a plain object explicitly:
function serializeError(err) {
return {
name: err.name,
message: err.message,
stack: err.stack,
code: err.code, // for Node.js system errors (ENOENT, etc.)
// Do NOT include: err.cause if it might be circular
};
}
// In an error handler:
app.use((err, req, res, next) => {
console.error(JSON.stringify(serializeError(err)));
res.status(500).json({ error: err.message });
});Detecting Circular References Before Serializing
Useful for debugging — check if an object is circular before trying to stringify it:
function isCircular(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 => isCircular(v, seen));
}
console.log(isCircular(normalObj)); // false
console.log(isCircular(circularObj)); // trueChoosing the Right Fix
| Scenario | Best fix |
|---|---|
| Logging / error reporting | Custom replacer or json-stringify-safe |
| State management (need to restore) | flatted |
| Serializing Express req/res | Extract specific fields to a DTO |
| Error objects | Build a plain {name, message, stack} object |
| Mongoose/Sequelize models | Call .toObject() or .toJSON() first |
Common Culprits in Node.js Projects
- Express `req` and `res` — massive circular ref graphs; never serialize directly
- Mongoose model instances — call
.toObject()or.toJSON()before serializing - Sequelize instances — call
.get({ plain: true }) - DOM elements — parent/child node references
- React fiber objects — internal React tree has many circular refs
- Linked lists and trees with parent pointers — classic algorithm data structures
- EventEmitter instances — internal listener maps can form cycles
Use JSONKit's formatter at /json-formatter to paste any serialized JSON output and validate it was captured correctly.