"Converting circular structure to JSON" is one of the most frustrating errors you'll encounter when debugging JavaScript applications. You're trying to serialize an object — whether to log it, send it to an API, or store it — and the runtime throws an error because the object contains a reference to itself, directly or indirectly. It stops your logging dead, breaks error reporting, and makes it impossible to inspect state when you need it most.
The problem isn't actually JSON.stringify itself; it's that most objects in real applications contain circular references by design. DOM elements reference their parents and children. ORM models reference related records. Mongoose documents have internal state loops. Understanding why this happens, how to detect it, and the patterns to fix it will save you hours of debugging frustration.
What Is a Circular Reference?
A circular reference occurs when an object (or a chain of objects) eventually references itself. The simplest example:
const obj = {};
obj.self = obj;
JSON.stringify(obj);
// TypeError: Converting circular structure to JSON
Here obj points to itself directly. Circular references can be indirect — object A references B, B references C, and C references A. They're also common in realistic codebases:
class Node {
constructor(value) {
this.value = value;
this.parent = null;
this.children = [];
}
addChild(child) {
this.children.push(child);
child.parent = this; // child now references its parent
}
}
const root = new Node('root');
const child = new Node('child');
root.addChild(child);
JSON.stringify(root);
// TypeError: Converting circular structure to JSON
When you try to serialize root, JSON.stringify encounters root.children[0].parent, which points back to root, triggering the error.
When and Why This Happens in Production
You'll hit this error in several contexts:
Logging frameworks. Libraries like Winston or Bunyan serialize objects to JSON before sending them to files or services. If you pass an object with circular references to a logger, it fails.
Error tracking. When you're debugging production errors, you might capture context objects that include model instances, DOM nodes, or other structures with cycles.
API responses. If your backend tries to return an object tree with parent-child relationships without careful serialization, your response middleware will choke.
Testing and mocking. Mock objects or spies that reference the original object or each other can create unexpected cycles.
The error is a hard stop — your logger fails silently, your error report never ships, or your API returns a 500. And because the error often happens in background processes (logging, monitoring), you might not see it until production.
Detecting Circular References Before They Break Things
The first line of defense is catching the problem before JSON.stringify fails. You can write a simple validator:
function hasCircular(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return false;
}
if (seen.has(obj)) {
return true; // Found a cycle
}
seen.add(obj);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (hasCircular(obj[key], seen)) {
return true;
}
}
}
return false;
}
const obj = {};
obj.self = obj;
console.log(hasCircular(obj)); // true
This function walks the object tree and uses a WeakSet to track what it's already visited. If it encounters the same object twice, there's a cycle. Run this check before passing objects to JSON.stringify in critical paths — logging, error reporting, API serialization.
WeakSet is ideal here because it doesn't prevent garbage collection of the objects you're tracking. If you used a regular Set, you'd leak memory by holding references to every object you've ever serialized.
Fix 1: The WeakSet Replacer Pattern
JSON.stringify accepts a replacer function as its second argument. This function is called for every key-value pair and can skip or transform values. You can use a WeakSet inside the replacer to track objects you've already serialized:
function stringifyWithoutCircular(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'; // Or undefined, or skip the key
}
seen.add(value);
}
return value;
});
}
const root = new Node('root');
const child = new Node('child');
root.addChild(child);
console.log(stringifyWithoutCircular(root));
// {"value":"root","parent":null,"children":[{"value":"child","parent":"[Circular]","children":[]}]}
This approach preserves the tree structure you want while replacing circular references with a marker string (or omitting them entirely). It's lightweight, works in all JavaScript environments, and gives you control over how to represent the cycle.
If you want to skip the circular property entirely instead of marking it:
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return undefined; // Omit the key entirely
}
seen.add(value);
}
return value;
});
Choose your strategy based on your use case. Use [Circular] if you want visibility into where the cycle exists. Use undefined if you only care about acyclic data. For API responses, you might want neither — restructure your data before sending it.
Fix 2: Prefilter or Exclude Problematic Properties
Sometimes you don't need the circular property at all. If you're logging a DOM element, you don't need its parent reference. If you're serializing a database model, you might only need specific fields:
function stringifyModel(model) {
return JSON.stringify(model, (key, value) => {
// Skip properties that commonly cause cycles
if (key === 'parent' || key === '_owner' || key === '__proto__') {
return undefined;
}
return value;
});
}
Or extract only the fields you care about:
const cleaned = {
id: model.id,
name: model.name,
children: model.children.map(child => ({ id: child.id, name: child.name }))
};
JSON.stringify(cleaned); // No cycles, no problems
This is the most pragmatic fix in many cases. Know your data structure and serialize only what you need.
Fix 3: Use Libraries That Handle It
If you're serializing complex objects frequently, consider a library that strips cycles by default:
Flatted (github.com/WebReflection/flatted) — serializes and deserializes objects with circular references by flattening them.
Circular-JSON — older but stable, designed specifically for this problem.
Morgan/Winston + custom formatters — logging libraries often support custom serializers you can hook in.
For most new projects, the WeakSet replacer pattern is cleaner than adding a dependency, but if you're working with deeply nested, highly circular object graphs, a library can save time.
Debugging Circular References with LightTrace
Even with good serialization, circular references can hide bugs. When a logging or error-reporting step suddenly fails, how do you read the stack trace and figure out what object caused it? This is where error tracking becomes invaluable.
LightTrace captures full stack traces, breadcrumbs, and context from your application. When your serialization fails, you see exactly what was being logged, what the chain of calls was, and what conditions led to the error. You can set up alert rules to notify you of serialization failures before they impact customers, and use the AI-powered root-cause analysis to understand what object structure is causing the problem.
With source maps enabled, LightTrace links each stack frame directly to your code on GitHub, so you can jump straight to the line where you're trying to serialize the problematic object. No more guessing — you see the exact context.
Silent failures are the worst. If your error tracking silently fails to serialize an error object and the report never ships, you're flying blind. Monitor your monitoring — set up alerts for serialization failures so they don't go unnoticed.
Summary
Circular references are inevitable in real applications, but they're solvable:
- Detect them proactively with a WeakSet-based validator.
- Serialize safely using the WeakSet replacer pattern in JSON.stringify.
- Exclude properties you don't actually need.
- Test your logging and error reporting with realistic object shapes.
The key insight is that JSON.stringify isn't the problem — it's doing exactly what it's designed to do. Your job is to prepare the data before it reaches JSON.stringify, know what circular structures exist in your application, and handle them gracefully.
Start tracking errors in minutes
Production errors are hard to debug. Start tracking them with LightTrace — get full stack traces, source maps, breadcrumbs, and AI-powered root-cause analysis for every error. Start free.