How do I fix "Converting circular structure to JSON"?

Dark navy cover card with the title 'How do I fix "Converting circular structure to JSON"?' in white and a green accent bar

"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 pick error.message and error.response?.data.
  • Server request and response objects. Passing req or res to res.json() or a logger hits the socket that points back to the response.
  • Parent and child links. Trees with parent pointers, 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.

Sources