A circular JSON error can mean you are serializing an object that owns runtime relationships rather than just the data you want to transfer. Select a deliberate data object. A cycle-removing serializer is a different contract and can silently discard information.
Reproduce the boundary in a disposable fixture
Build a deliberate transfer object instead of stringifying a framework or request object.
Use a new empty temporary directory for this fixture. The example creates only local test data. Commands marked with example.invalid do not identify a real service.
const assert = require('node:assert/strict');
const state = {count:2};
const owner = {state};
owner.self = owner;
assert.throws(() => JSON.stringify(owner),TypeError);
const dto = {count:owner.state.count};
assert.equal(JSON.stringify(dto),'{"count":2}');
// A repeated reference without a cycle is valid.
const shared = {label:'same'};
const text = JSON.stringify({left:shared,right:shared});
const parsed = JSON.parse(text);
assert.deepEqual(parsed.left,parsed.right);
assert.notEqual(parsed.left,parsed.right);
console.log(text);Observed result
{"left":{"label":"same"},"right":{"label":"same"}}What the result means
The owner has a path back to itself and fails serialization. The transfer object contains only the intended count and succeeds. The repeated-reference example also succeeds, but the two parsed children are separate objects. This distinguishes an actual cycle from merely having the same object in two places, and shows that ordinary JSON is not a mechanism for preserving in-memory object identity.
Choose the correction for your workflow
Locate the boundary that calls JSON.stringify: an HTTP response, log field, cache entry or message payload. Inspect the expected schema and construct only those fields from the runtime object. For an HTTP handler, this often means returning a result body rather than the entire request or response wrapper. For framework state, read the documented state export interface rather than assuming every store property belongs on the wire. Validate the selected data and keep private fields out of logs. A successful stringify is necessary for that representation, but not sufficient to prove that the correct data was selected.
Limits and the next check
The fixture is plain JavaScript on the recorded Node version, not a tested fix for every Express middleware or framework version. A WeakSet-based replacer can incorrectly drop repeated non-cyclic references if it treats all seen objects as cycles. A special graph format requires a compatible decoder. util.inspect may help local diagnosis, but its output is not a substitute for a stable JSON API contract.
Related: Compare the related workflow boundary.
This isolated example was executed on macOS on November 10, 2026 (KST). Available tools: node v26.5.0, git git version 2.39.3 (Apple Git-145), npm 11.17.0, jq jq-1.7.1-apple. Shell fixtures use Bash and repository-path identity uses Python 3. This checks the illustrated contract, not your project, host service or every tool version.