TypeError: Converting circular structure to JSON

JSON is a tree: a value cannot contain itself. When an object links back to one of its ancestors, JSON.stringify would recurse forever, so it throws instead. V8 prints the path of the loop: the object where it starts, each property it follows, and the property that closes the circle. This sample is valid JavaScript, so it formats cleanly here; run it and it throws exactly that error.

Seen as:

  • TypeError: Converting circular structure to JSON
  • TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'lead' -> object with constructor 'Object' --- property 'team' closes the circle
  • TypeError: cyclic object value
  • TypeError: JSON.stringify cannot serialize cyclic structures.
  • ValueError: Circular reference detected

Input

Settings

History

Load from URL

Common causes

1. Parent and child objects pointing at each other

Trees with back-references (a node with parent, a team with a lead whose team points back) are circular by design. Leave the back-reference out with a replacer.

Before
const json = JSON.stringify(team);
After
const json = JSON.stringify(team, (key, value) => (key === 'team' ? undefined : value));

2. Logging framework objects such as req, res or an Axios error

Express requests, Node sockets and Axios errors link to each other (socket.parser.socket, error.request.res). Log the fields you need instead of the whole object.

Before
logger.info(JSON.stringify(error));
After
logger.info(
  JSON.stringify({
    message: error.message,
    status: error.response?.status,
    url: error.config?.url,
  }),
);

3. DOM nodes, events or React refs in state

A DOM element references its document, which references the element again. Store the values you need (an id, the input value) rather than the node or event.

Before
localStorage.setItem('lastClick', JSON.stringify(event));
After
localStorage.setItem('lastClick', JSON.stringify({ id: event.target.id, x: event.clientX, y: event.clientY }));

4. ORM or Mongoose documents with relations loaded

Entities with both sides of a relation populated form cycles, and model instances carry internal state. Convert to plain data first, for example with doc.toObject() or a DTO that lists the fields to send.

Before
res.send(JSON.stringify(order));
After
res.send(JSON.stringify({ id: order.id, total: order.total, customerId: order.customer.id }));

5. Generic fallback: drop repeated references

For debugging output, a replacer that remembers seen objects in a WeakSet prints each object once and marks the repeats. It also hides legitimately shared objects, so use it for logs, not for data you will parse again.

Before
console.log(JSON.stringify(state));
After
const seen = new WeakSet();
console.log(
  JSON.stringify(state, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  }),
);

Frequently asked questions

How do I read the "--> starting at object" lines?

They trace the loop: the object where it starts, then each property followed, and finally the property that points back to the start. The constructor names (Object, Socket, HTMLDivElement) tell you which kind of object is involved.

Does console.log have the same problem?

No. console.log and Node’s util.inspect detect cycles and print [Circular *1] instead of throwing. Only JSON serialization requires a tree.

Can JSON represent circular data at all?

Not directly. Formats such as JSON Pointer references or libraries like flatted encode cycles as references, but both sides must agree on the convention.

Related