"Converting circular structure to JSON" means you passed JSON.stringify an object that leads back to itself, and JSON has no way to write that loop. To fix converting circular structure to JSON errors, read the path in the error message, find the property that closes the circle, and then remove it, replace it, or serialize only the fields you need.
Why JSON.stringify throws on a circular structure
JSON.stringify throws because the JSON format can't express "this points to that object over there". MDN's cyclic object value page says the format doesn't support object references, so JSON.stringify() doesn't try to solve them and fails.
The same bug has a different message in each engine:
| Engine | Message |
|---|---|
| V8 (Chrome, Edge, Node) | TypeError: Converting circular structure to JSON |
| Firefox | TypeError: cyclic object value |
| Safari | TypeError: JSON.stringify cannot serialize cyclic structures. |
A cycle is a path that returns to one of its own ancestors. Here is a small one:
const user = { name: "Ana" };
const team = { name: "Core", members: [user] };
user.team = team; // user -> team -> members -> user
JSON.stringify(user); // TypeError
MDN's JSON.stringify reference lists two causes for a TypeError: a circular reference, or a BigInt value. If you see the BigInt one instead, the JSON.stringify undefined guide covers it.
How to read the error path in Chrome and Node
The V8 version of the error names the exact property that loops, so read the whole message before you change any code. Running the example above in Node prints this:
Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'team' -> object with constructor 'Object'
| property 'members' -> object with constructor 'Array'
--- index 0 closes the circle
Read it top to bottom as a walk through your data. It starts at user, goes into team, then into members, and members[0] is user again. The line ending in "closes the circle" is your culprit.
The constructor names help in real apps. A path through ServerResponse, Socket or ClientRequest tells you that you passed a whole HTTP object, not data. A class name from your ORM tells you a model instance went in with its connection attached.
Common places circular structures come from
Most circular structures in real code are framework objects that you didn't mean to serialize.
- HTTP client errors. An Axios error holds its request and response, which are full of circular links. The Axios issue that added toJSON() says Axios errors were not serializable by default because of those structures. Log
error.toJSON(), or pickerror.messageanderror.response?.data. - Server request and response objects. Passing
reqorrestores.json()or a logger hits the socket that points back to the response. - Parent and child links. Trees with
parentpointers, doubly linked lists and graph nodes are cyclic by design. - DOM nodes and events. Stringifying a click event or an element pulls in nodes that point to their parents.
Five ways to fix converting circular structure to JSON
Pick the fix by what you wanted the JSON for, not by which snippet is shortest.
| Goal | Fix |
|---|---|
| Send data to an API or save it | Serialize only the fields you need, or add toJSON() |
| Log an object to look at it | util.inspect in Node, or a replacer that marks loops |
| Deep copy an object | structuredClone |
| Save and restore the full graph | A library such as flatted |
1. Serialize only the fields you need
Build a plain object with the data the other side needs. For the user example, send { name: user.name, team: user.team.name }. For Axios, that's response.data, not the response. This is the fix I reach for first, because it also stops you from leaking internals into an API.
2. Add a toJSON method to the class
If the cyclic object is your own class, give it a toJSON() method. JSON.stringify calls it and writes whatever it returns. Return the fields you want and leave out the back-reference, for example toJSON() { return { name: this.name, teamName: this.team?.name }; }.
3. Use a replacer that marks the loop
A replacer can swap the looping value for a marker string. MDN's version tracks the current chain of ancestors and returns "[Circular]" only when a value is one of its own ancestors:
function getCircularReplacer() {
const ancestors = [];
return function (key, value) {
if (typeof value !== "object" || value === null) return value;
while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop();
if (ancestors.includes(value)) return "[Circular]";
ancestors.push(value);
return value;
};
}
JSON.stringify(user, getCircularReplacer());
// {"name":"Ana","team":{"name":"Core","members":["[Circular]"]}}
Many snippets online use a WeakSet of every object seen instead. That version also replaces shared objects that are not cycles. MDN's own test shows the difference: [o, o] is not circular, and the ancestor replacer writes it as [{},{}]. A seen-set replacer drops the second copy, which loses data you didn't need to lose.
MDN warns that filtering this way loses data: the JSON can't be turned back into the original object.
4. Use structuredClone when you only wanted a copy
If the stringify call was really JSON.parse(JSON.stringify(obj)) to deep copy, switch tools. MDN's JSON.stringify page suggests structuredClone(), which supports circular references. The structuredClone reference shows the cycle kept in the copy. It throws a DataCloneError if any part of the value can't be cloned, such as a function.
5. Use util.inspect for logs, flatted for round trips
For debugging in Node, util.inspect prints cycles instead of throwing. The Node.js util docs say circular references point to their anchor with a reference index:
<ref *1> {
name: 'Ana',
team: { name: 'Core', members: [ [Circular *1] ] }
}
When you need to store a cyclic graph and rebuild it later, use a library. flatted describes itself as a light circular JSON parser and offers stringify and parse. Its README warns that you must pair them: Flatted.parse(Flatted.stringify(data)) works, while mixing either side with plain JSON does not.
What these fixes can't do
None of these fixes makes standard JSON hold a real reference. A flatted string is still valid JSON text, but a normal JSON.parse on the other end gets index strings, not your object graph. That matters if a backend in another language reads the data, because it would need a matching decoder.
My take: if a server needs the data, reshape it into a tree with IDs, such as teamId: "core", instead of shipping a loop. IDs survive every language and every database.
Once the JSON is valid, paste it into the JSON viewer to check that the fields you expected are there. If the receiving side then fails to read it, the unexpected end of JSON input guide and JSON.stringify vs JSON.parse cover the parse side.
FAQ
What does Converting circular structure to JSON mean?
It means the value you passed to JSON.stringify contains an object that leads back to itself through its own properties. JSON has no way to write a reference, so JSON.stringify throws a TypeError instead of looping forever.
How do I find which property is circular?
Read the rest of the error message in Chrome or Node. It lists the path from the starting object, one property per line, and the line that ends in "closes the circle" names the property that points back.
Is the same object used twice a circular reference?
No. An array like [o, o] holds the same object twice but never loops, and JSON.stringify writes it as two copies. Only a path that leads back to an ancestor is a cycle.
How do I deep copy an object with circular references?
Use structuredClone instead of JSON.parse(JSON.stringify(x)). structuredClone supports circular references and keeps them in the copy, but it throws a DataCloneError for values it can't clone.