Automattic/mongoose · error · MongooseError
a circular reference in the update value, updateValue: ${uti
Error message
a circular reference in the update value, updateValue:
${util.inspect(recursion.raw.update, { showHidden: false, depth: 1 })}
updatePath: '${recursion.raw.path}' What it means
Before applying an update, Mongoose flattens nested update objects (lib/helpers/common.js) for casting and diffing. It tracks visited objects in a WeakSet; encountering the same object again means the update value contains a circular reference, which could never be serialized to BSON, so it throws MongooseError with a util.inspect snapshot (depth 1) of the top-level update and the path where the cycle was detected.
Source
Thrown at lib/helpers/common.js:85
/*!
* ignore
*/
function modifiedPaths(update, path, result, recursion = null) {
if (update == null || typeof update !== 'object') {
return;
}
if (recursion == null) {
recursion = {
raw: { update, path },
trace: new WeakSet()
};
}
if (recursion.trace.has(update)) {
throw new MongooseError(`a circular reference in the update value, updateValue:
${util.inspect(recursion.raw.update, { showHidden: false, depth: 1 })}
updatePath: '${recursion.raw.path}'`);
}
recursion.trace.add(update);
const keys = Object.keys(update || {});
const numKeys = keys.length;
result = result || {};
path = path ? path + '.' : '';
for (let i = 0; i < numKeys; ++i) {
const key = keys[i];
let val = update[key];
const _path = path + key;
result[_path] = true;
if (!Buffer.isBuffer(val) && isMongooseObject(val)) {
val = val.toObject({ transform: false, virtuals: false });View on GitHub (pinned to 49cdab0136)
Solutions
- Build a plain, acyclic DTO for the update: map only the fields you want to persist.
- Store references instead of embeddings: replace cycles with ObjectId refs, e.g. $set: { parent: parentNode._id }.
- Detect and strip cycles with a walker before calling update APIs.
Example fix
// before
const node = { name: 'root' };
node.self = node;
await Tree.updateOne({ _id: id }, { $set: node }); // throws: circular reference
// after
await Tree.updateOne({ _id: id }, { $set: { name: 'root' } });
// or store a reference instead of embedding:
await Tree.updateOne({ _id: childId }, { $set: { parent: rootNode._id } }); Defensive patterns
Strategy: validation
Validate before calling
function hasCycle(value) {
const seen = new WeakSet();
function walk(v) {
if (v == null || typeof v !== 'object') return false;
if (seen.has(v)) return true;
seen.add(v);
return Object.values(v).some(walk);
}
return walk(value);
}
if (hasCycle(update)) {
throw new Error('update payload contains a circular reference');
}
await Model.updateOne(filter, update); Type guard
function isAcyclicPayload(update) {
return !hasCycle(update); // hasCycle defined in validationCode
} Try / catch
try {
await Model.updateOne(filter, update);
} catch (err) {
if (err instanceof mongoose.MongooseError && err.message.includes('circular reference')) {
// rebuild the update as a plain DTO (pick explicit fields) and retry once
} else {
throw err;
}
} Prevention
- Never pass live object graphs to update APIs; construct literal update objects at the call site.
- Keep parent/child back-references as ObjectIds, not embedded objects.
- Sanitize external payloads with schema validators (zod/joi) that rebuild plain objects.
When it happens
Trigger: Model.updateOne(filter, { $set: obj }) where obj contains itself (obj.self = obj); update payloads built from entity or graph structures carrying parent back-references; nested documents whose child points back at an ancestor object.
Common situations: Passing ORM-ish or in-memory graph objects straight into updates; building hierarchical data (menus, trees) in place; accidentally assigning child.parent = parentNode and then embedding parentNode in the update.
Related errors
- Invalid project() argument. Must be string or object
- `transform` function must be synchronous, but the transform
- Infinite subdocument loop: subdoc with _id ${doc._id} is a p
- Invalid atomic update value for ${op}. Expected an object, r
- Invalid update pipeline operator: "${op}"
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/43cc1b5bb4e4dd9e.
Report an issue: GitHub.