Skip to content
JSON Tidy
JSON errors

TypeError: Converting circular structure to JSON

You get this error when the object you pass to JSON.stringify contains a reference back to itself. In the example below, a folder lists its files and each file points back to its folder. JSON has no way to write a reference, so JSON.stringify would go around that loop forever, and it throws instead. In Node.js and Chrome, the lines under the error show which property creates the loop.

Quick fix

  1. Look at the last line of the message. It names the property that points back to an object that’s already being converted.
  2. If the message starts at a Socket, you passed in a request, a response, or an HTTP client error. Send only the fields you need from it.
  3. If you’re only logging the object, use console.log(obj) instead of console.log(JSON.stringify(obj)). Node and browsers can print objects that contain loops.

Reading the message

Here’s a small version of the problem. The folder has a list of children, and each child has a parent property that points back to the folder:

const docs = { name: "docs", children: [] };
const intro = { name: "intro.md", parent: docs };
docs.children.push(intro);

JSON.stringify(docs);

In Node.js and Chrome, the error looks like this:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'children' -> object with constructor 'Array'
    |     index 0 -> object with constructor 'Object'
    --- property 'parent' closes the circle

The indented lines trace a path from the object you passed in. Starting at the folder, JSON.stringify went into children, then into the first item, and that item’s parent property led back to the folder. That last property is the one to deal with, either by leaving it out of the JSON or by not converting this object at all.

Common causes

Express requests and responses

This is probably the most common case. res.json(req), or JSON.stringify(req) inside a log statement, fails with a message that starts at a Socket:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Socket'
    |     property 'parser' -> object with constructor 'HTTPParser'
    --- property 'socket' closes the circle

The request holds the network socket it arrived on, and the socket holds a reference back to the request. You rarely want the whole request anyway, so build an object with the parts you care about:

// Instead of res.json(req)
res.json({ method: req.method, url: req.originalUrl, body: req.body });

axios errors

When an axios request fails, the error carries the request that was sent in error.request and the response in error.response. Both lead back to the same socket, so passing either one to JSON.stringify fails. The message, the status code, and the response body are usually all you need:

try {
  await axios.get(url);
} catch (error) {
  console.error(JSON.stringify({
    message: error.message,
    status: error.response?.status,
    data: error.response?.data,
  }));
}

DOM elements and React events

An element points to its parent and its children, and React adds its own references on top, so JSON.stringify(event.target) fails. Read the value you need, like event.target.value, and store or send that instead.

Your own objects

Trees where each node knows its parent, doubly linked lists, and models that refer to each other, such as a user with orders where each order points back to its user, all contain loops on purpose. For these, you decide what the JSON should look like, which the next section shows.

Leaving out the back-reference with toJSON

If one of your classes has a property like parent, add a toJSON method. JSON.stringify calls it and converts whatever it returns, so you can return everything except the property that points back up:

class Folder {
  constructor(name, parent) {
    this.name = name;
    this.parent = parent;
    this.children = [];
    parent?.children.push(this);
  }
  toJSON() {
    return { name: this.name, children: this.children };
  }
}

const docs = new Folder("docs");
new Folder("guides", docs);
JSON.stringify(docs);
// {"name":"docs","children":[{"name":"guides","children":[]}]}

Dropping parent doesn’t lose any information, because the nesting in the JSON already shows which folder each child belongs to. If you need the links again after JSON.parse, you can set them as you walk through the tree.

Logging an object that has loops

When you’re debugging, you probably don’t need a JSON string at all. console.log and Node’s util.inspect can both print objects with loops, and they mark the spot where the loop closes with [Circular *1]:

import util from "node:util";

console.log(util.inspect(docs, { depth: 4 }));
// <ref *1> {
//   name: 'docs',
//   children: [ { name: 'intro.md', parent: [Circular *1] } ]
// }

Sometimes you do need a string, for example when a logging service only accepts text. A replacer function can skip objects that have already been written:

const seen = new WeakSet();
JSON.stringify(docs, (key, value) => {
  if (typeof value === "object" && value !== null) {
    if (seen.has(value)) return undefined;
    seen.add(value);
  }
  return value;
});
// {"name":"docs","children":[{"name":"intro.md"}]}

Keep in mind that this skips every object it has seen before, not just the ones that form a loop. If the same object appears in two places, it only shows up the first time. That’s acceptable in a log, but not in data another program will read.

The same error elsewhere

FirefoxTypeError: cyclic object value
SafariTypeError: JSON.stringify cannot serialize cyclic structures.
PythonValueError: Circular reference detected
Gojson: unsupported value: encountered a cycle via *main.Node
PHP, json_last_error_msg()Recursion detected