JSON Debugging

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.

Quick Fix
  1. 1. Use a custom replacer with WeakSet to detect and replace circular refs with "[Circular]"
  2. 2. For lossless round-trips, use the flatted library
  3. 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 JSON

Mutual 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 JSON

Indirect 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 JSON

Why 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);  // true

Install: 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);  // true

Pros: 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?

SolutionRound-TripDependenciesBest For
Custom ReplacerNo (data lost)NoneLogging, debugging
flattedYes (lossless)flatted (~1KB)Internal storage, caching
structuredClonePartialNone (modern browsers)Quick clone before stringify
$ref PatternYes (lossless)cycle or custom codeAPI communication, cross-system

Common Pitfalls

Using a regular Set instead of WeakSet in the replacer
A regular Set holds strong references to objects, preventing garbage collection. Always use WeakSet — it holds weak references and allows objects to be garbage collected when no longer needed.
Forgetting that the replacer receives the transformed value, not the original
The replacer function receives the value after previous transformations. If you modify values in the replacer, be aware that nested objects may already be processed. Always check the value type before adding it to the WeakSet.
Assuming flatted output is standard JSON
flatted produces valid JSON but uses a $ref convention that other JSON parsers do not understand. You cannot send flatted output to a non-JavaScript backend and expect it to work without a compatible parser.
Using JSON.parse(JSON.stringify(obj)) for deep cloning
This common pattern throws on circular references. Use structuredClone() instead, or a library like lodash.cloneDeep that handles circular references.
Not handling circular references in error logging
If you log error objects (which often have circular references) with JSON.stringify, your error handler itself will crash. Always use a safe stringify function in logging code.
Ignoring DOM node circular references
DOM nodes have many internal circular references (parentNode, ownerDocument, etc.). Never try to stringify a DOM node directly. Extract the data you need into a plain object first.

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 flatted or the $ref pattern.
  • Use our JSON Validator to check your JSON output, or JSON Formatter to beautify it.

Related Resources