Automattic/mongoose · error · Error

Invalid atomic update value for ${op}. Expected an object, r

Error message

Invalid atomic update value for ${op}. Expected an object, received ${typeof val}

What it means

castUpdate requires each update operator's value to be a non-null object (and not a Buffer) — that object maps field paths to values. After $set-sugar normalization, if an operator in mongodbUpdateOperators ($set, $inc, $push, $pull, $unset, $currentDate, $min, $max, $mul, $rename, $setOnInsert, $addToSet, $pop, $pullAll, $bit) has a non-object value, Mongoose throws a plain Error telling you the received typeof. Documents are auto-converted with toObject() first, so only genuine non-objects reach the check.

Source

Thrown at lib/helpers/query/castUpdate.js:153

  // cast each value
  i = ops.length;
  while (i--) {
    const op = ops[i];
    val = ret[op];
    hasDollarKey = hasDollarKey || op.startsWith('$');
    if (val?.$__) {
      val = val.toObject(internalToObjectOptions);
      ret[op] = val;
    }
    if (val &&
        typeof val === 'object' &&
        !Buffer.isBuffer(val) &&
        mongodbUpdateOperators.has(op)) {
      walkUpdatePath(schema, val, op, options, context, filter);
    } else {
      const msg = 'Invalid atomic update value for ' + op + '. '
          + 'Expected an object, received ' + typeof val;
      throw new Error(msg);
    }

    if (op.startsWith('$') && utils.isEmptyObject(val)) {
      delete ret[op];
    }
  }

  if (utils.hasOwnKeys(ret) === false &&
      options.upsert &&
      utils.hasOwnKeys(filter)) {
    // Trick the driver into allowing empty upserts to work around
    // https://github.com/mongodb/node-mongodb-native/pull/2490
    // Shallow clone to avoid passing defaults in re: gh-13962
    return { $setOnInsert: { ...filter } };
  }
  return ret;
};

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Give every operator an object of path→value pairs: { $inc: { count: 1 } }, { $unset: { field: '' } }
  2. JSON.parse incoming update payloads and assert the shape before passing them to updateOne/updateMany/findOneAndUpdate
  3. For replacement-style updates, omit operators entirely and pass the plain doc to Model.replaceOne / findOneAndReplace

Example fix

// before
User.updateOne({ _id }, { $inc: 1 });

// after
User.updateOne({ _id }, { $inc: { count: 1 } });
Defensive patterns

Strategy: validation

Validate before calling

const UPDATE_OPS = new Set(['$set','$unset','$inc','$dec','$mul','$min','$max','$rename','$setOnInsert','$push','$pull','$addToSet','$pop','$pullAll','$currentDate','$bit']);
function assertUpdateShape(update) {
  for (const [op, v] of Object.entries(update)) {
    if (UPDATE_OPS.has(op) && (v == null || typeof v !== 'object' || Buffer.isBuffer(v))) {
      throw new Error(`${op} value must be an object of { field: value }`);
    }
  }
}

Type guard

type UpdateOp = Record<string, Record<string, unknown>>;
function isShapedUpdate(u: unknown): u is UpdateOp {
  return typeof u === 'object' && u !== null && !Array.isArray(u) &&
    Object.entries(u).every(([k, v]) => !k.startsWith('$') || (typeof v === 'object' && v !== null && !Buffer.isBuffer(v)));
}

Try / catch

try { await Model.updateOne(f, u); } catch (err) { if (/Invalid atomic update value/.test(err.message)) { /* the message names the op and typeof received — wrap the value in { field: value } */ } throw err; }

Prevention

When it happens

Trigger: Model.updateOne(filter, { $inc: 1 }); { $set: 'name' }; { $push: Buffer.from(...) }; { $unset: '' }; { $set: null } — any operator whose value is a string, number, boolean, null/undefined, or Buffer.

Common situations: Flattening { $set: { count: 1 } } into { $set: 1 } when refactoring; passing a serialized update from an API body without parsing; reusing classic $unset string syntax ({ $unset: 'field' }) which is invalid — $unset needs { field: '' }.

Related errors


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