When JSON.stringify() encounters a circular reference, it throws a TypeError instead of producing JSON. This often happens when an object graph includes a parent link, a self-reference, or framework objects that refer back to one another. The right fix depends on whether you need a JSON API payload or need to preserve the object graph.
Key Takeaways
- JSON represents nested values, but it has no syntax for object references or cycles.
- Choose and serialize the fields an API actually needs; do not blindly delete properties from the original object.
- If you need to preserve circular references, use a purpose-built serializer and make sure the reader uses a compatible parser.
- A replacer that remembers every object it has seen can incorrectly treat repeated, non-circular references as cycles.
Why JSON.stringify() reports a circular structure error
JSON values can be objects, arrays, strings, numbers, booleans, and null. Objects and arrays can nest, but JSON has no built-in way to say “this value points to the same object as that earlier value.” As a result, a cyclic JavaScript object cannot be represented as ordinary JSON. MDN documents this limitation and the resulting error.
A direct cycle is easy to create:
const user = { name: "Ari" };
user.self = user;
JSON.stringify(user); // Throws a TypeError
An indirect cycle has the same problem:
const parent = {};
const child = { parent };
parent.child = child;
JSON.stringify(parent); // Throws a TypeError
The exact error text varies by JavaScript engine, but the cause is the same: following object properties eventually leads back to an object already on the current traversal path.
Repeated references are different from cycles. They are valid JavaScript, and JSON.stringify() can serialize them, but the output contains duplicate data rather than a reference to the original object:
const shared = { code: "x1" };
const value = { first: shared, second: shared };
JSON.stringify(value);
// '{"first":{"code":"x1"},"second":{"code":"x1"}}'
After parsing that JSON, first and second are separate objects. JSON preserves the values, not JavaScript object identity.
First decide what the output is for
For an API request, log record, or persisted JSON document, the goal is usually to send a deliberate set of data—not every property reachable from a complex runtime object. Project the source into a plain data shape before serializing it.
For example, instead of serializing a whole user object that may include a circular manager link, select the fields the receiving API needs:
function toUserPayload(user) {
return {
id: user.id,
name: user.name,
managerId: user.manager?.id ?? null,
};
}
const payload = toUserPayload(user);
const body = JSON.stringify(payload);
This approach makes the output shape explicit and avoids accidentally exposing unrelated properties. It also lets you represent relationships as identifiers, such as managerId, rather than embedding an entire connected object graph.
Choose fields instead of mutating the source object
Avoid fixing the error by deleting properties directly from production data:
// Risky: changes the object other code may still be using.
delete user.manager;
That can break later code, alter shared state, or remove information another operation needs. A safer alternative is to construct a new payload, as in the previous example. For a small one-off object, destructuring can also create a copy without the unwanted property:
const { manager, ...userWithoutManager } = user;
const body = JSON.stringify(userWithoutManager);
Use this only if removing manager is correct for the output. It is a shallow operation: other properties may still contain circular references. For nested data, build an explicit projection or use a serializer designed for the full object graph.
Use a circular-reference-aware serializer when identity matters
If the requirement is to preserve cycles or repeated references—for example, when storing and restoring a JavaScript object graph—ordinary JSON is the wrong format on its own. A library such as flatted provides a compatible stringify and parse pair:
npm install flatted
import { parse, stringify } from "flatted";
const original = { name: "Ari" };
original.self = original;
const encoded = stringify(original);
const restored = parse(encoded);
console.log(restored.self === restored); // true
Use the library's parser to read its serialized output. Do not assume that output can be parsed with native JSON.parse() and recover the same references, or that an unrelated service understands the library's encoding. For a public API that requires standard JSON, prefer a field projection or change the API's data model to use IDs for relationships.
Why a simple WeakSet replacer can be misleading
A common suggestion is to pass a replacer to JSON.stringify() that omits every object already seen in a WeakSet. That can prevent an exception, but it can also omit legitimate repeated references. The shared example above would be treated as if its second occurrence were a cycle, even though there is no path back to the object.
There is another trade-off: omitting a value changes the output and may silently lose information. A replacer is appropriate only when that omission is an intentional, documented part of the serialization format. It is not a general-purpose way to preserve circular structure.
If the goal is an in-memory deep copy rather than JSON output, consider structuredClone() in environments that support it. It can handle circular references, but it does not turn the object into JSON or make a JSON API payload possible.
Common Mistakes
- Assuming every repeated object is circular. A shared reference and a cycle are different; native JSON duplicates a shared object's data.
- Deleting a property from the original object just to make serialization succeed. Create a separate, intentional output object instead.
- Dropping every previously seen object with a replacer. That can discard valid repeated data and change the payload unexpectedly.
- Using a circular-aware serializer for a standard JSON API without checking compatibility. The receiving system needs to understand the same serialization format.
- Trying
JSON.parse()on the original failing value. Parsing is a separate operation; it cannot serialize a JavaScript object or resolve its cycles.
Best Practices
- Define the data contract first: which fields should the output contain?
- Convert complex application objects into plain payload objects at the API or persistence boundary.
- Represent relationships as stable identifiers when the consumer does not need nested object identity.
- Use a compatible circular-reference serializer only when preserving graph identity is a real requirement.
- Test both the serialized output and the parsed result, including repeated references or cycles if they matter.
Conclusion
JSON.stringify() fails on cycles because standard JSON cannot express object references. For most APIs, build a fresh payload containing only the fields the consumer needs. When preserving cycles is essential, use a purpose-built serializer and its matching parser rather than silently stripping data.
