Node.js — TypeError: Converting Circular Structure to JSON
Diagnose Converting circular structure to JSON when stringifying objects with cycles, find the back-reference in request or ORM objects, and serialize a safe projection.
Symptoms
- TypeError: Converting circular structure to JSON appears when logging or responding with a nested object
- The failure shows up only for certain entities, for example ones that have a parent or sibling relation loaded
- The stack points at JSON.stringify inside a logger or res.json call, not at application logic
Possible causes
- An ORM entity with bidirectional relations (order.customer and customer.orders) is passed to JSON.stringify
- A request or response object was attached to a child property during request context plumbing
- A tree structure stores a parent pointer on every node (node.parent) alongside children (node.children)
- A cache stores the object and later links back to the cache entry itself
Diagnosis steps
Locate the exact cycle with a safe stringifier
node -e "const s = require('util').inspect(obj, { depth: 3 }); console.log(s.slice(0, 2000))"Interpretation
util.inspect renders circular references as [Circular *1] markers, so the first marker sits next to the property that closes the loop.
Trace which property carries the back-pointer
console.log(Object.keys(order.customer)) # look for a key that returns the original objectInterpretation
A key whose value is the outer object itself is the back-reference that stringify cannot follow.
Confirm which call site serializes it
NODE_OPTIONS=--stack-trace-limit=50 node server.js # then read the stringify frameInterpretation
The frame tells you whether a logger, res.json, or a queue payload is the consumer that must receive a projection instead.
Root cause
JSON is a tree format, so JSON.stringify traverses objects depth-first and cannot encode a node that points back to an ancestor. Any back-reference in the object graph turns into this TypeError at the moment of serialization, regardless of how correct the data itself is.
Fix
Serialize an explicit projection of the fields consumers need instead of whole ORM or request objects. Where a generic safe path is required, pass a replacer that skips known back-reference keys, or use a serializer that emits [Circular] markers for diagnostics only.
Risk note
A blanket replacer that silently drops cycles can hide data-model problems and leak unintended fields into responses. Prefer explicit projections for API payloads.
Verify the fix
Re-run the failing request with the projection in place and assert a 200 with the expected JSON body, plus a logger smoke test that prints the entity without throwing.
Symptom walkthrough
The error fires at the serialization boundary, and the message names the crime precisely:
TypeError: Converting circular structure to JSON
at JSON.stringify (<anonymous>)
at toPayload (src/routes/orders.js:58:17)
The object itself is fine; only its shape is unserializable. Print it with util.inspect, which annotates the loop instead of throwing:
{ id: 7, customer: { name: 'Ada', orders: [ [Circular *1] ] } }
[Circular *1] sits exactly on the property that points back to the ancestor, which is usually a bidirectional ORM relation or a parent pointer.
Fix in practice
For API responses, serialize a projection so the contract is explicit:
app.get('/orders/:id', async (req, res) => {
const order = await db.order.findById(req.params.id, { withCustomer: true });
res.json({
id: order.id,
customer: { name: order.customer.name }
});
});
For debug logging where you want the whole graph, use inspect or a replacer that cuts known back-edges:
const SAFE_KEYS = new Set(['parent', '_request', 'customer', 'owner']);
const safe = JSON.stringify(obj, (k, v) => (SAFE_KEYS.has(k) ? '[skipped]' : v), 2);
Never send replacer-stripped output to clients; the dropping rules are for humans reading logs, not for a stable API contract.
Verification
Request the previously failing entity and assert the response is 200 with exactly the projected fields. Run the logger over an entity with loaded relations and confirm no throw. Finally, add a guard in tests: deep-scan serialized fixtures for Circular markers so a new relation added to the model cannot re-arm the failure unnoticed.