Converting circular structure to JSON means JSON.stringify followed a property path back to the same object. Check self-refs, Node req/res, DOM events, and component instances. Send a DTO; use structuredClone only when you need a copy.
TypeError: Converting circular structure to JSON comes from JSON.stringify, not JSON.parse. JSON is text. It has no object pointers. If a property path walks back to the same object, stringify stops. V8 (current Node and Chrome) prints that path, such as property 'me' closes the circle. Firefox says cyclic object value. Safari says JSON.stringify cannot serialize cyclic structures.
Read the property names in the error, then change the payload shape. Paste a cycle-free sample into the JSON formatter. If you need a payload that actually stringifies, export one from the test data generator. If the failure is on parse, see Unexpected token in JSON at position 0.
Do not know how to serialize a BigInt is a different TypeError. That value is a BigInt, not a cycle.
V8 draws the walk from the start object to the close. property 'me' is a self-reference. property 'child' -> ... property 'parent' is two objects pointing at each other. constructor 'Array' with index 0 means the array contains itself. If it starts at constructor 'Socket' and then parser, you probably passed req or res. Log the top-level keys first instead of stringifying the whole graph.
The same object twice is not a cycle. [{ id: 1 }, { id: 1 }] is fine. So is const o = {}; JSON.stringify([o, o]). A cycle means you can follow properties back to the same value.
For debugging, a replacer can mark a cycle as [Circular]. Use an ancestor stack, not a WeakSet. A WeakSet also flags shared references such as [o, o].
function jsonSafe(value) {
const ancestors = []
return JSON.stringify(value, function (_key, v) {
if (typeof v !== 'object' || v === null) return v
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
ancestors.pop()
}
if (ancestors.includes(v)) return '[Circular]'
ancestors.push(v)
return v
})
}
That helper is for logs. A public API still needs ordinary JSON with no cycles. The other side's JSON.parse will not turn [Circular] back into a reference.
You stringified Likely cause
Your { me: ... } Self-ref or parent/child
Express / Koa req / res Socket and HTTPParser point at each other
window, or a DOM node with a fiber window.window, or a React back-pointer
A component, $el, or fiber Framework back-pointers
const self = {}
self.me = self
JSON.stringify(self)
// TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'Object'
// --- property 'me' closes the circle
A graph node or a cache often hangs a field like this. An API usually needs an id or a foreign key. delete self.me unblocks this stringify call, but the next assignment puts the cycle back.
const parent = { id: 1 }
const child = { parent }
parent.child = child
JSON.stringify(parent)
// ... property 'child' -> object with constructor 'Object'
// --- property 'parent' closes the circle
Trees, comment threads, and ORM preload often keep both parent and children. Keep one direction in the response: parents expose childIds, or children expose parentId. If both sides need a full object, copy one side and drop the back-pointer.
Express and Koa attach the request and response to a net.Socket. In current Node, stringify of req or res starts at the socket:
// Inside http.createServer:
JSON.stringify(req)
// TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'Socket'
// | property 'parser' -> object with constructor 'HTTPParser'
// --- property 'socket' closes the circle
The cycle is socket.parser and parser.socket. Many snippets write req.socket.req. That is a shortcut, and it does not match the message Node prints now. Log req.method, req.url, and req.headers. res throws the same way.
On Axios failures, read error.response.data and error.config.url. In the browser, error.request is often an XMLHttpRequest, and stringify usually turns that field into {}.
const arr = []
arr.push(arr)
JSON.stringify(arr)
// --> starting at object with constructor 'Array'
// --- index 0 closes the circle
A mistaken push of the outer array does this. Adjacency lists and homemade queues hit it too. Send a list of ids instead of pushing the array into itself.
window.window === window. In current Chrome, JSON.stringify(window) starts at constructor 'Window' and closes on property 'window'.
Stringifying a bare DOM node often does not throw. parentNode and children live on the prototype. JSON.stringify only walks enumerable own properties, so JSON.stringify(document.body) is often {}. The call succeeds and drops the node.
A node with a React fiber does throw. The path looks like __reactFiber$... → FiberNode → stateNode back to the element. new Event('click') is usually {"isTrusted":false}. A homemade { target: event.currentTarget } becomes { target: {} } if the target is a bare node. Log event.type and event.target.id. To snapshot markup, pick the tags and attributes yourself, or copy outer HTML in the browser.
JSON.parse(JSON.stringify(obj)) throws on a cycle. Functions, undefined, and Symbol on objects disappear. undefined in an array becomes null. A Date becomes an ISO string and comes back as a string, not a Date.
To copy plain data that may contain cycles, use structuredClone(obj). It keeps circular refs and returns an object, not a JSON string. Functions and DOM nodes throw DataCloneError. So do req and component instances. The HTTP body still has to be cycle-free fields.
Vue $el / $parent, React fiber return / child / sibling, or a store that holds a component will walk back into the framework. Devtools can show that tree. The response body cannot.
Serialize props, state, or a DTO you own. Leave this, the component instance, and vnode out of JSON.stringify.
Libraries such as flatted write an extended format with index arrays. The other side's JSON.parse will not rebuild your cycles, and it will not understand those arrays. For local logs, the [Circular] replacer above is enough. For a payload that must parse, use the test data generator. The JSON formatter only flags JSON that already stringified. While the cycle still lives on the object, the formatter cannot help.
Related: JSON formatter · Blog index