jsonerrorsjavascriptnodedebugging

Converting Circular Structure to JSON — Cause & Fix

·8 min read·Error Fixes

What Is a Circular Structure?

A circular reference exists when an object has a property that points back to itself — directly or through a chain of other objects. JSON.stringify() cannot serialize this because it would have to follow the reference forever, so it throws:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'self' -> object with constructor 'Object'
    --- property 'self' closes the circle

Modern Node.js (v12+) shows the full cycle path in the error message, which makes it easier to find.

How Circular References Are Created

Direct self-reference:

javascript
const obj = { name: "Ravi" };
obj.self = obj; // obj.self points back to obj itself
JSON.stringify(obj); // TypeError

Parent-child cycle (common in trees and linked lists):

javascript
const parent = { name: "Parent", children: [] };
const child  = { name: "Child", parent };  // child holds a ref to parent
parent.children.push(child);
// Traversal: parent → children[0] → parent → children[0] → ...infinite
JSON.stringify(parent); // TypeError

Express/Koa request and response objects:

javascript
const express = require("express");
app.get("/debug", (req, res) => {
  res.json(req);  // TypeError — req has circular refs internally
});

Node.js Error objects:

javascript
const err = new Error("failed");
JSON.stringify(err);        // "{}" — empty! Error properties are non-enumerable
JSON.stringify({ message: err.message }); // works fine

Fix 1: Custom Replacer with WeakSet (Zero Dependencies)

The cleanest zero-dependency solution — replaces circular references with the string "[Circular]":

javascript
function safeStringify(obj, indent = 2) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) return "[Circular]";
      seen.add(value);
    }
    return value;
  }, indent);
}

// Usage
const obj = { name: "Ravi" };
obj.self = obj;
console.log(safeStringify(obj));
// {"name":"Ravi","self":"[Circular]"}

This is safe to use in production logging, error reporting, and debugging.

Fix 2: The json-stringify-safe npm Package

Drop-in replacement for JSON.stringify that handles circular references:

bash
npm install json-stringify-safe
javascript
const stringify = require("json-stringify-safe");

const json = stringify(circularObj, null, 2);
// Circular refs become "[Circular ~]" with path information

This is the most battle-tested option for logging libraries and API serializers.

Fix 3: flatted for Round-Trip Serialization

flatted encodes circular references in a format that can be decoded back to the original structure — unlike the replacer approach which loses the circular ref permanently:

bash
npm install flatted
javascript
import { stringify, parse } from "flatted";

const parent = { name: "Parent", children: [] };
const child  = { name: "Child", parent };
parent.children.push(child);

const encoded = stringify(parent);   // handles circular refs
const decoded = parse(encoded);      // restores the full structure including refs
// decoded.children[0].parent === decoded  (true — same reference)

Use flatted when you need to serialize/deserialize complex object graphs (e.g., state management, undo history) without data loss.

Fix 4: Remove the Circular Reference Before Serializing

If you control the data structure, the cleanest fix is to design it without back-references:

javascript
// Instead of: child holds a reference to parent
const parent = { name: "Parent", children: [] };
const child  = { name: "Child" };  // no parent pointer here
parent.children.push(child);
JSON.stringify(parent); // works fine

Or project to a plain DTO (Data Transfer Object) that only contains the fields you want to serialize:

javascript
const dto = {
  id:    user.id,
  name:  user.name,
  email: user.email,
  // intentionally omits: user.account (would create a circular ref)
};
JSON.stringify(dto);

Fix 5: Serializing Error Objects Correctly

Error objects have non-enumerable properties — JSON.stringify produces {}. Build a plain object explicitly:

javascript
function serializeError(err) {
  return {
    name:    err.name,
    message: err.message,
    stack:   err.stack,
    code:    err.code,     // for Node.js system errors (ENOENT, etc.)
    // Do NOT include: err.cause if it might be circular
  };
}

// In an error handler:
app.use((err, req, res, next) => {
  console.error(JSON.stringify(serializeError(err)));
  res.status(500).json({ error: err.message });
});

Detecting Circular References Before Serializing

Useful for debugging — check if an object is circular before trying to stringify it:

javascript
function isCircular(obj, seen = new WeakSet()) {
  if (typeof obj !== "object" || obj === null) return false;
  if (seen.has(obj)) return true;
  seen.add(obj);
  return Object.values(obj).some(v => isCircular(v, seen));
}

console.log(isCircular(normalObj));   // false
console.log(isCircular(circularObj)); // true

Choosing the Right Fix

ScenarioBest fix
Logging / error reportingCustom replacer or json-stringify-safe
State management (need to restore)flatted
Serializing Express req/resExtract specific fields to a DTO
Error objectsBuild a plain {name, message, stack} object
Mongoose/Sequelize modelsCall .toObject() or .toJSON() first

Common Culprits in Node.js Projects

  • Express `req` and `res` — massive circular ref graphs; never serialize directly
  • Mongoose model instances — call .toObject() or .toJSON() before serializing
  • Sequelize instances — call .get({ plain: true })
  • DOM elements — parent/child node references
  • React fiber objects — internal React tree has many circular refs
  • Linked lists and trees with parent pointers — classic algorithm data structures
  • EventEmitter instances — internal listener maps can form cycles

Use JSONKit's formatter at /json-formatter to paste any serialized JSON output and validate it was captured correctly.

Try JSON Formatter

Inspect your JSON structure to spot circular references before serializing.