Automattic/mongoose · error · MongooseError
Invalid update pipeline operator: "${op}"
Error message
Invalid update pipeline operator: "${op}" What it means
Mongoose whitelists pipeline stages in update pipelines: only $unset, $project, $addFields, $set, $replaceRoot and $replaceWith are allowed. Any other top-level key in any stage of the array throws MongooseError 'Invalid update pipeline operator: "<op>"'. This is stricter than the MongoDB server, which also permits stages like $match and $addFields variants in updates — Mongoose rejects them client-side.
Source
Thrown at lib/helpers/query/castUpdate.js:202
if (op === '$project') {
if (val == null || typeof val !== 'object') {
throw new MongooseError('Invalid $project in pipeline, must be an object');
}
return val;
}
if (op === '$addFields' || op === '$set') {
if (val == null || typeof val !== 'object') {
throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');
}
return val;
} else if (op === '$replaceRoot' || op === '$replaceWith') {
if (val == null || typeof val !== 'object') {
throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');
}
return val;
}
throw new MongooseError('Invalid update pipeline operator: "' + op + '"');
}
/**
* Walk each path of obj and cast its values
* according to its schema.
*
* @param {Schema} schema
* @param {object} obj part of a query
* @param {string} op the atomic operator ($pull, $set, etc)
* @param {object} [options]
* @param {boolean|'throw'} [options.strict]
* @param {Query} context
* @param {object} filter
* @param {string} pref path prefix (internal only)
* @return {Bool} true if this path has keys to update
* @api private
*/
View on GitHub (pinned to 49cdab0136)
Solutions
- Move filtering into the update's first argument (the filter), keep only allowed stages in the array
- Use Model.aggregate() when you need $group/$match/other stages — then merge/write with $merge or $out if needed
- Check each stage key against the allowlist ($unset, $project, $addFields, $set, $replaceRoot, $replaceWith) before sending
Example fix
// before
User.updateMany({}, [{ $match: { status: 'active' } }, { $set: { archived: true } }]);
// after: filter goes in the query, not the pipeline
User.updateMany({ status: 'active' }, [{ $set: { archived: true } }]); Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = new Set(['$unset','$project','$addFields','$set','$replaceRoot','$replaceWith']);
for (const stage of pipeline) {
for (const op of Object.keys(stage)) {
if (!ALLOWED.has(op)) throw new Error(`${op} not allowed in a Mongoose update pipeline`);
}
} Type guard
const UPDATE_PIPELINE_STAGES = ['$unset','$project','$addFields','$set','$replaceRoot','$replaceWith'] as const;
type UpdatePipelineStage = { [K in typeof UPDATE_PIPELINE_STAGES[number]]?: Record<string, unknown> }; Try / catch
try { await Model.updateOne(f, pipeline); } catch (err) { if (/Invalid update pipeline operator/.test(err.message)) { /* move $match-style logic into the filter, use aggregate() for the rest */ } throw err; } Prevention
- Put filter conditions in the query, not as $match stages
- Use Model.aggregate() for $group/$match and write results via $merge
- Validate stage names against the Mongoose allowlist before sending
When it happens
Trigger: Model.updateOne({}, [{ $match: { status: 'active' } }, { $set: { archived: true } }]); [{ $group: {...} }]; any stage key outside the allowlist, including typos like '$Set' or '$unsetField'.
Common situations: Pasting an aggregation pipeline into updateOne expecting it to work; trying to filter with $match inside an update pipeline instead of the query filter; assuming all aggregation stages are supported in update pipelines because MongoDB accepts some Mongoose does not.
Related errors
- Mongoose does not support using async iterators with an exis
- a circular reference in the update value, updateValue: ${uti
- Invalid atomic update value for ${op}. Expected an object, r
- Field `${pathToCheck}` is not in schema and strict mode is s
- Field `${prefix}${key}` is not in schema and strict mode is
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/3c994b64470cd755.
Report an issue: GitHub.