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

  1. Build a plain, acyclic DTO for the update: map only the fields you want to persist.
  2. Store references instead of embeddings: replace cycles with ObjectId refs, e.g. $set: { parent: parentNode._id }.
  3. 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

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


AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21). Data as JSON: /api/errors/43cc1b5bb4e4dd9e. Report an issue: GitHub.