Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool
Troubleshooting JSON Circular Reference Errors
If JSON.stringify() throws TypeError: Converting circular structure to JSON, the value you are serializing contains a reference loop. One object points back to itself directly, or a chain of properties eventually points back to an earlier object. JSON has no built-in way to represent that graph, so serialization stops with an error instead of recursing forever.
The fastest way to fix it is to decide what you actually need: return a plain JSON-safe object for an API response, use a cycle-aware replacer for lossy export, use util.inspect() if you only need debug output, or use structuredClone() if you were trying to deep-copy data rather than serialize it.
What the Error Actually Means
A circular reference exists when an object property leads back to an object that is already in the current traversal path.
Direct cycle
const user = { name: "Ava" };
user.self = user;
JSON.stringify(user);
// TypeError: Converting circular structure to JSONIndirect cycle
const team = { name: "Core" };
const member = { name: "Lee" };
team.member = member;
member.team = team;
JSON.stringify(team);
// TypeError: Converting circular structure to JSONCurrent engines do not all format the message the same way. MDN documents examples such as Chrome and Node.js reporting Converting circular structure to JSON, Firefox reporting cyclic object value, and Safari reporting a message that references a circular structure.
Circular Reference vs Repeated Reference
This distinction matters because a lot of "safe stringify" snippets on the web get it wrong. Reusing the same object twice is not automatically a circular reference.
Shared object: valid JSON, no cycle
const shared = { id: 42 };
const payload = {
first: shared,
second: shared,
};
JSON.stringify(payload, null, 2);
// Works.
// The same data is duplicated in the output JSON.A replacer based only on a global WeakSet or WeakMap will often remove repeated references even when there is no cycle. That can silently change the data. For a true troubleshooting guide, the safer rule is: track the current ancestor chain, not every object ever seen.
Common Real-World Causes
- ORM entities with two-way relations: a user contains posts and each post contains the same user.
- Express, request, or response objects: framework objects often contain large internal graphs and parent links.
- DOM nodes and browser events: many browser objects contain internal references that are not JSON-safe.
- Application state with parent pointers: trees that store both children and parent references create cycles by design.
- Debug logging helpers: the failure often appears inside a logger, network helper, or cache layer rather than at the original data-construction site.
How to Find the Offending Reference Fast
Do not start by trying random stringify helpers. First identify whether the value should be serialized at all, then locate the back-reference.
- Reproduce the error with the smallest possible object, not the full app state or framework object.
- If you are in Node.js, inspect the value with
util.inspect(obj, { depth: null })instead of stringifying it. That prints circular references safely for debugging. - Check for obvious back-links like
parent,owner, or framework internals. - Confirm whether you have a real cycle or only a repeated reference that appears in multiple branches.
Small helper to locate the first cycle path
function findFirstCyclePath(value) {
function visit(node, path, ancestors) {
if (!node || typeof node !== "object") {
return null;
}
const existing = ancestors.find((entry) => entry.node === node);
if (existing) {
return `${path} points back to ${existing.path}`;
}
const nextAncestors = [...ancestors, { node, path }];
for (const [key, child] of Object.entries(node)) {
const result = visit(child, `${path}.${key}`, nextAncestors);
if (result) {
return result;
}
}
return null;
}
return visit(value, "$", []);
}
const team = { name: "Core" };
const member = { name: "Lee", team };
team.member = member;
console.log(findFirstCyclePath(team));
// $.member.team points back to $That helper is intentionally simple. It is useful for your own plain objects and arrays, but framework objects may still require inspecting a reduced copy because some properties are non-enumerable or extremely large.
Fix Options That Match the Job
1. Return a plain JSON-safe shape
This is usually the right fix for APIs, server actions, logs sent to external systems, and anything else that must become real JSON. Build a DTO or plain response object instead of serializing a rich domain object.
Example: remove the back-reference
const user = {
id: 1,
name: "Ava",
posts: [
{ id: 101, title: "Hello" },
{ id: 102, title: "World" },
],
};
const response = {
id: user.id,
name: user.name,
posts: user.posts.map((post) => ({
id: post.id,
title: post.title,
})),
};
JSON.stringify(response, null, 2); // Safe2. Use a cycle-aware replacer when lossy JSON is acceptable
If you only need a best-effort export or readable log line, you can replace circular branches with a marker such as "[Circular]". The key detail is using the current ancestor stack so shared non-circular objects are preserved.
Example: safer replacer pattern
function getCircularReplacer() {
const ancestors = [];
return function replacer(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;
};
}
const team = { name: "Core" };
const member = { name: "Lee", team };
team.member = member;
console.log(JSON.stringify(team, getCircularReplacer(), 2));This produces usable JSON, but it is a lossy representation. You have removed graph information, so do not use it for round-tripping business data unless that tradeoff is intentional.
3. Use util.inspect() for debugging in Node.js
If your goal is just to log or inspect the value, JSON is the wrong tool. Node's util.inspect() prints complex objects safely and marks circular references instead of throwing.
Example: debug safely without JSON
import util from "node:util";
const team = { name: "Core" };
const member = { name: "Lee", team };
team.member = member;
console.log(util.inspect(team, { depth: null, colors: false }));4. Use structuredClone() when you wanted a deep copy
Developers often hit this error because they are using JSON.parse(JSON.stringify(value)) as a cloning trick. That approach was always limited and it fails on cycles. If you need a deep copy of supported data, structuredClone() is the better built-in option and it can handle circular references.
Common Mistakes to Avoid
- Stringifying framework objects directly: serialize only the data you own, not the full request, response, event, or element object.
- Using a global seen-set replacer blindly: that often removes valid repeated references, not just cycles.
- Treating debug output as transport data:
util.inspect()output is for humans, not APIs. - Keeping bidirectional links in response payloads: use IDs or flattened shapes at the boundary instead.
Bottom Line
Troubleshooting JSON circular reference errors gets easier once you separate three questions: is this value actually supposed to become JSON, where does the back-reference appear, and do you need a real transport format or only readable debug output? For production data boundaries, reshape the object. For lossy export, use a cycle-aware replacer. For debugging, inspect instead of stringifying. For cloning, use structuredClone().
Need help with your JSON?
Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool