The error message is trying to be helpful. Most people never read past the first line.
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'comment' closes the circle
That second and third line are the actual diagnosis. The object has a property called comment, and that property points back at the object itself. JSON.stringify follows the reference around and around until it gives up. The fix is never in the stringify call. It is in whatever built the object.
I learned this on a cart service that saved to localStorage on every update. One day a _meta field got stamped onto each item, referencing the payload object that contained the items themselves. Every save() crashed. The checkout still worked, because page state was fine in memory. Only the persistence path threw, silently, in the background. The bug report said "cart doesn't survive refresh." The real bug was a reference loop three properties deep.
Where the loop hides
Circular references rarely look circular when you write the code. The common shapes:
A child object that stores a pointer back to its parent. Tree structures do this constantly. The parent has children, each child has parent, and the tree serializes forever.
Framework or ORM objects. An error object from nodemailer carries issuerCertificate, which links to a chain of certificates that link back. console.log handles it. JSON.stringify does not.
Debug metadata stamped onto live objects. The _meta case above: someone attached the whole payload to each item for tracing, mutating the very objects being serialized.
DOM nodes and class instances. Anything with ownerDocument, event listeners, or a looping prototype chain is not stringifyable.
The fastest way to find yours is the error text itself. V8 names the property that closes the circle. Read it.
The one copyable fix for a circular structure
If you must serialize the object as-is, pass JSON.stringify a replacer that tracks objects it has already seen and drops the repeat:
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return undefined;
}
seen.add(value);
}
return value;
};
};
JSON.stringify(data, getCircularReplacer(), 2);
WeakSet is the right container because it lets garbage collection do its job once the stringify finishes. Returning undefined for a repeated object drops that property from the output, which breaks the loop. This is the triage option. It ships the data, minus the cycles.
What you give up with each option
There are four real fixes and they are not equal.
The replacer above is the quickest and the lossiest. It silently deletes properties. If a downstream system needed the parent pointer, it is gone and nothing tells you. Use it for logs, crash reports, and debug dumps, not for data you persist.
flatted (an npm package) keeps the references by serializing the object graph into a flat array format with pointers. Nothing is lost, and flatted.parse restores the original loop. The catch: the output is not plain JSON anyone else can read. It only round-trips through flatted itself. If the consumer is your own code, that is fine. If the consumer is a third party API, it is a non-starter.
Field whitelisting is the fix I reach for in production code. Instead of serializing the whole object, build the exact shape you need: const payload = { id: item.id, name: item.name, qty: item.qty }. The circular reference cannot survive because the looping properties never get copied. This is also the only fix that keeps junk like SSL certificate chains and DOM internals out of your logs.
A custom toJSON method on the class is the cleanest when the loop comes from your own object model. Define what the object looks like as data, once, and every JSON.stringify call in the codebase benefits.
My ranking: whitelist first, toJSON if you own the class, flatted if you need lossless round-trips in your own system, the replacer only for logs. Most circular-structure bugs are data-model smells, and the replacer is a bandage. The cart bug above died permanently when save() built an explicit payload instead of stringifying live state.
Where this breaks down
Two places. First, the replacer drops data silently, so never use it on a payload another system will read as the source of truth. Second, flatted's output looks like JSON and parses like JSON, but the pointer entries mean a plain JSON.parse gives you a confusing array instead of your object. If you ever paste flatted output into a validator expecting standard JSON, you will get a confusing afternoon.
One more thing the error message teaches: the circle is always one property assignment away. When a serialize call starts throwing, look at the most recent line that mutates the object, not at the stringify call.
A tool that repairs invalid JSON text is a different fix for a different problem. If the text itself is malformed, like single quotes, trailing commas, or raw newlines inside strings, paste it into the repair tool and it will fix the text directly.
Frequently asked questions
Why does JSON.stringify throw on circular structures?
JSON as a format is a tree: every value nests inside its parent, and there is no syntax for a reference that points back up. When stringify follows a property that points back to an object it already visited, it has nowhere to write the loop, so it throws TypeError: Converting circular structure to JSON.
Can I just delete the property that closes the circle?
Yes, if nothing needs it. The error names the property that closes the circle, so delete that property or build an explicit payload of only the fields you need. That is the permanent fix: field whitelisting means the looping property never reaches the stringify call.
Does structuredClone help with circular references?
structuredClone copies circular objects without complaining, but the clone is still circular. JSON.stringify on the clone throws exactly the same error. structuredClone solves deep copying, not serialization.
Is try/catch around JSON.stringify a valid fix?
Only as a guard so one bad object does not crash a logger or a request. It does not produce usable data: the stringify still fails and you get nothing. Wrap serialization in try/catch for resilience, but fix the object for correctness.
Is there a JSON format that supports circular references?
Plain JSON, no. The npm package flatted serializes the object graph into a flat array with pointer entries that preserve the loops, and flatted.parse restores them. The output only round-trips through flatted itself, so it works when both sides are your code and not when a third party expects standard JSON.
Get one practical dev-tools guide a week: subscribe to the newsletter for new JSON guides and parser explainers.
Related reading: Fixing Unescaped Quotes in JSON String Values · How to Fix the JSON Single Quotes Error · The Trailing Comma That Breaks Your JSON · Why Is My JSON Invalid?