Fix "Converting Circular Structure to JSON" Error in JavaScript
The TypeError: Converting circular structure to JSON error means your object contains a reference that loops back to itself. Here are 4 practical solutions to handle circular references.
- 1. Use a custom replacer with
WeakSetto detect and replace circular refs with"[Circular]" - 2. For lossless round-trips, use the
flattedlibrary - 3. For modern browsers, use
structuredClone()to create a safe copy first
What is a Circular Reference?
A circular reference occurs when an object references itself, either directly or through a chain of properties. This creates an infinite loop when traversing the object.
Direct Self-Reference
An object has a property that points to itself
const obj: Record<string, unknown> = { name: "Alice" };
obj.self = obj; // points back to itself
// obj → obj.self → obj.self.self → ... (infinite loop)
JSON.stringify(obj);
// TypeError: Converting circular structure to JSONMutual Reference
Two objects reference each other
const alice: Record<string, unknown> = { name: "Alice" };
const bob: Record<string, unknown> = { name: "Bob" };
alice.friend = bob;
bob.friend = alice; // mutual reference
// alice → bob → alice → bob → ... (infinite loop)
JSON.stringify(alice);
// TypeError: Converting circular structure to JSONIndirect Reference
A chain of properties eventually loops back
const parent: Record<string, unknown> = { name: "Parent" };
const child: Record<string, unknown> = { name: "Child" };
parent.child = child;
child.parent = parent; // back to parent
// parent → child → parent → child → ... (infinite loop)
JSON.stringify(parent);
// TypeError: Converting circular structure to JSONWhy JSON.stringify Fails
JSON.stringify() recursively walks through every property of an object and converts it to a string. When it encounters a circular reference, it enters an infinite loop — it keeps following the same path over and over, never reaching the end.
JavaScript detects this and throws a TypeError instead of crashing or running forever. This is a safety mechanism, not a bug.
// The traversal looks like this: // obj.name → "Alice" ✓ // obj.self → obj → obj.name → "Alice" ✓ // obj.self → obj → obj.self → obj → ... // ↑ infinite loop detected → TypeError thrown
Solution 1: Custom Replacer Function
The most common approach. Use a WeakSet to track objects you have already visited, and replace circular references with a placeholder string.
function safeStringify(obj: unknown, space?: number): string {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
// Primitives are always safe
if (typeof value !== 'object' || value === null) {
return value;
}
// Check if we've seen this object before
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
return value;
}, space);
}
// Usage:
const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj;
console.log(safeStringify(obj, 2));
// {
// "name": "Alice",
// "self": "[Circular]"
// }Pros: No dependencies, works everywhere, easy to understand.
Cons: Data is lost at the circular point — you cannot reconstruct the original object from the output.
Solution 2: Using the flatted Library
The flatted library is a drop-in replacement for JSON that handles circular references using a $ref pattern internally. It can round-trip data without loss.
import { parse, stringify } from 'flatted';
const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj;
// Serialize — no error!
const json = stringify(obj);
console.log(json);
// {"name":"Alice","self":{"$ref":"$"}}
// Parse — circular reference is restored!
const restored = parse(json);
console.log(restored.self === restored); // trueInstall: pnpm add flatted
Pros: Lossless round-trip, drop-in API, lightweight (~1KB).
Cons: Output is not standard JSON — other systems need flatted to parse it.
Solution 3: structuredClone (Modern Browsers)
structuredClone() can clone objects with circular references. While it does not produce JSON, you can use it to create a safe copy, then remove the circular properties before stringifying.
const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj;
// Clone preserves the circular reference
const clone = structuredClone(obj);
console.log(clone.self === clone); // true
// Remove circular properties before stringifying
function removeCircular<T>(obj: T, seen = new WeakSet()): T {
if (typeof obj !== 'object' || obj === null) return obj;
if (seen.has(obj)) return undefined as T;
seen.add(obj);
const clean: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
clean[key] = removeCircular(value, seen);
}
return clean as T;
}
const safe = removeCircular(obj);
console.log(JSON.stringify(safe, null, 2));
// {
// "name": "Alice",
// "self": null
// }Pros: No library needed, handles many edge cases (Date, RegExp, Map, Set, ArrayBuffer).
Cons: Not available in Node.js < 17 or older browsers. Does not produce JSON directly.
Solution 4: $ref Pattern for API Communication
When you need to send circular objects over the network and reconstruct them on the other side, use the decycle / retrocycle pattern. This replaces circular references with $ref JSON Pointers.
// decycle: replace circular refs with $ref JSON Pointers
function decycle(obj: unknown, seen = new Map()): unknown {
if (typeof obj !== 'object' || obj === null) return obj;
if (seen.has(obj)) {
return { $ref: seen.get(obj) };
}
const path = seen.size === 0 ? '$' : '';
seen.set(obj, path);
if (Array.isArray(obj)) {
return obj.map((item, i) => {
seen.set(obj, seen.get(obj) || '$');
return decycle(item, new Map([...seen, [obj, `${seen.get(obj) || '$'}[${i}]`]]));
});
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
result[key] = decycle(value, new Map([...seen, [obj, `${seen.get(obj) || '$'}.${key}`]]));
}
return result;
}
// retrocycle: restore circular refs from $ref pointers
function retrocycle(obj: unknown): unknown {
const refs: Record<string, unknown> = { $: obj };
function resolve(value: unknown): unknown {
if (value && typeof value === 'object') {
const ref = (value as Record<string, unknown>).$ref as string | undefined;
if (typeof ref === 'string') return refs[ref];
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
(value as Record<string, unknown>)[k] = resolve(v);
if (v && typeof v === 'object' && !(v as Record<string, unknown>).$ref) {
refs[`$${k}`] = v;
}
}
}
return value;
}
return resolve(obj);
}
// Usage:
const obj: Record<string, unknown> = { name: 'Alice' };
obj.self = obj;
const decycled = decycle(obj);
const json = JSON.stringify(decycled);
// {"name":"Alice","self":{"$ref":"$"}}
// On the receiving end:
const restored = retrocycle(JSON.parse(json));
console.log(restored.self === restored); // truePros: Produces standard JSON, works across languages, full round-trip support.
Cons: More code to maintain, or you can use the cycle npm package which provides this out of the box.
Which Solution Should You Use?
| Solution | Round-Trip | Dependencies | Best For |
|---|---|---|---|
| Custom Replacer | No (data lost) | None | Logging, debugging |
| flatted | Yes (lossless) | flatted (~1KB) | Internal storage, caching |
| structuredClone | Partial | None (modern browsers) | Quick clone before stringify |
| $ref Pattern | Yes (lossless) | cycle or custom code | API communication, cross-system |
Common Pitfalls
Frequently Asked Questions
What does "Converting circular structure to JSON" mean?
It means you tried to call JSON.stringify() on an object that contains a circular reference — a property that eventually points back to the object itself. JSON.stringify cannot handle circular references because it would loop infinitely.
How do I fix a circular reference in JSON.stringify?
The simplest fix is to use a custom replacer function with a WeakSet to detect and replace circular references with "[Circular]". For a more robust solution, use the flatted library which can serialize and deserialize circular structures without data loss.
Can structuredClone handle circular references?
Yes, structuredClone() can clone objects with circular references. However, it clones the object — it does not produce a JSON string. You would use structuredClone to create a safe copy, then remove or break the circular references before calling JSON.stringify.
What is the flatted library?
flatted is a lightweight JavaScript library that provides drop-in replacements for JSON.stringify and JSON.parse that handle circular references. It uses a $ref pattern internally to represent repeated and circular references, and can round-trip data without loss.
Why does JSON.stringify not support circular references by design?
JSON is a tree-structured data format — each value appears exactly once. Circular references create a graph structure, which cannot be represented in standard JSON without a convention like $ref. Adding built-in support would break the JSON specification and compatibility with all JSON parsers.
Key Takeaways
- Circular references happen when an object references itself directly or through a chain of properties.
JSON.stringify()cannot handle them — it throws a TypeError as a safety mechanism.- For debugging and logging, use a custom replacer with
WeakSet. - For lossless round-trips, use
flattedor the$refpattern. - Use our JSON Validator to check your JSON output, or JSON Formatter to beautify it.