Fixing 'TypeError: Converting circular structure to JSON' in Node.js

intermediate๐Ÿ’š Node.js2026-04-20| Node.js (All versions), Browser JavaScript (V8, SpiderMonkey, etc.)

Error Message

TypeError: Converting circular structure to JSON
#json#circular-reference#serialization#nodejs

TL;DR: The Quick Fix

If you need to log an object without crashing your app, use a custom replacer function. This snippet uses a WeakSet to track and skip objects it has already visited.

const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) return;
      seen.add(value);
    }
    return value;
  };
};

const jsonString = JSON.stringify(yourObject, getCircularReplacer());

Why This Error Happens

The TypeError: Converting circular structure to JSON triggers when JSON.stringify() encounters an object that references itself. JSON is strictly a tree structure. When you introduce a circular reference, you create a graph with cycles that the standard algorithm cannot traverse to an end point.

Imagine this simple, broken setup:

const user = { name: 'Alice' };
user.self = user; // This creates an infinite loop

JSON.stringify(user); // Throws TypeError

You will hit this frequently in Node.js when working with complex instances. For instance, an Express request object (req) contains hundreds of internal references to the server and socket. Similarly, Sequelize or Mongoose models often link back to their parent connection, and DOM elements link to their parents, who link back to the children.

Practical Solutions

1. The WeakSet Replacer (Best for Logging)

This is the most efficient way to print an object to the console or a log file. It uses a WeakSet to store references. Since WeakSet doesn't prevent garbage collection, it's memory-efficient. If the replacer sees the same object twice, it simply discards the second reference.

function safeStringify(obj) {
  const cache = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (cache.has(value)) return; 
      cache.add(value);
    }
    return value;
  });
}

2. Using the 'flatted' Library (Best for Data Persistence)

Sometimes you need to actually save the data and restore it later. If you strip the circular parts, you lose that data forever. The flatted library solves this by replacing circular references with unique identifiers during serialization and restoring them during parsing.

It's a small package, roughly 1.1KB gzipped. Install it via npm:

npm install flatted

Usage is identical to the native JSON global:

const { stringify, parse } = require('flatted');

const circularObj = { a: 1 };
circularObj.itself = circularObj;

const serialized = stringify(circularObj);
const deserialized = parse(serialized);

console.log(deserialized.itself === deserialized); // true

3. Node.js util.inspect (Best for Debugging)

Don't reach for JSON.stringify if you only need to see what's inside a variable. Node's native util.inspect is built for this. It detects cycles automatically and marks them as [Circular] instead of crashing.

const util = require('util');
const complexObject = { /* ... */ };

// depth: null ensures it shows all nested levels
console.log(util.inspect(complexObject, { depth: null, colors: true }));

How to Verify the Fix

Before deploying your fix, run through these checks:

  • Test for silence: Wrap the call in a try-catch. If your replacer is working, the catch block should no longer trigger.
  • Inspect the output: If you used the WeakSet approach, verify that the missing keys are actually the ones you intended to skip.
  • Check for Nulls: Stripping references can sometimes leave you with null values that might break frontend components expecting specific data.

Prevention and Best Practices

Debugging deep objects is easier when you can see the structure. Tools like the JSON Formatter & Validator at ToolCraft allow you to paste data chunks to spot where a child node points back to a parent. Since it processes data locally in your browser, your config files stay private.

To avoid these errors entirely:

  • Use DTOs (Data Transfer Objects): Never stringify a raw database model. Instead, map it to a plain object containing only the fields you need (e.g., id, email, name).
  • Keep State Flat: In React or Redux, circular references are usually a red flag for architectural issues. Aim for a flat, serializable state.
  • Manual Pruning: If you know a specific key like user.manager is causing the loop, delete it or set it to undefined before calling stringify.

Further Reading

Related Error Notes