Automattic/mongoose · warning · MongooseError
Aggregate has empty pipeline
Error message
Aggregate has empty pipeline
What it means
findOneAndUpdate() and findOneAndReplace() historically accepted { new: true } to get the updated document back. Mongoose normalized this through convertNewToReturnDocument(): it warns, maps new:true -> returnDocument:'after' and new:false -> 'before', then deletes the new key. Behavior is unchanged; the warning pushes you to the canonical returnDocument option.
Source
Thrown at lib/aggregate.js:804
/**
* Execute the aggregation with explain
*
* #### Example:
*
* Model.aggregate(..).explain()
*
* @param {'queryPlanner'|'executionStats'|'allPlansExecution'} [verbosity]
* @return {Promise}
*/
Aggregate.prototype.explain = async function explain(verbosity) {
if (typeof verbosity === 'function' || typeof arguments[1] === 'function') {
throw new MongooseError('Aggregate.prototype.explain() no longer accepts a callback');
}
const model = this._model;
if (!this._pipeline.length) {
throw new MongooseError('Aggregate has empty pipeline');
}
prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
const preFilter = buildMiddlewareFilter(this.options, 'pre');
const postFilter = buildMiddlewareFilter(this.options, 'post');
// Remove middleware option before passing to MongoDB
const options = this.options != null ? { ...this.options } : {};
delete options.middleware;
try {
await model.hooks.execPre('aggregate', this, [], { filter: preFilter });
} catch (error) {
return await model.hooks.execPost('aggregate', this, [null], { error, filter: postFilter });
}
const cursor = await model.collection.aggregate(this._pipeline, options);View on GitHub (pinned to 49cdab0136)
Solutions
- Rename the option: { new: true } -> { returnDocument: 'after' }; { new: false } or omission stays 'before' by default.
- Apply a codemod across the repo: grep for "new: true" near findOneAndUpdate/findOneAndReplace/findByIdAndUpdate and replace.
- Do not mix styles per query — standardize on returnDocument to keep greppability.
Example fix
// before
const doc = await Model.findOneAndUpdate(filter, update, { new: true });
// after
const doc = await Model.findOneAndUpdate(filter, update, { returnDocument: 'after' }); Defensive patterns
Strategy: validation
Validate before calling
const AFTER = { returnDocument: 'after' };
// one shared constant instead of inline { new: true }
const doc = await Model.findOneAndUpdate(filter, update, { ...AFTER, runValidators: true }); Prevention
- Write { returnDocument: 'after' } from the start in new code; treat { new: true } as legacy.
- Codemod existing call sites: search 'new: true' near *AndUpdate queries.
- Encapsulate query options in typed wrappers (TS: Pick<QueryOptions, 'returnDocument'>) so 'new' cannot sneak back in.
When it happens
Trigger: await Model.findOneAndUpdate(filter, update, { new: true }); the object form findByIdAndUpdate(id, update, { new: true, runValidators: true }); any codebase patterned on pre-6 docs/tutorials that used new: true.
Common situations: Extremely common in legacy code — { new: true } was the idiomatic form for years; Stack Overflow answers copied verbatim; gradual migrations where some queries already use returnDocument; test snapshots polluted by the warning text.
Related errors
- Aggregate pipeline for $unionWith cannot include `$out` or `
- Aggregate.prototype.explain() no longer accepts a callback
- Aggregate `near()` argument must have a `near` property
- Aggregate `near()` argument has invalid coordinates, got "${
- Provided object has both field "${name}" and its alias "${al
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/92b46c31063acf5a.
Report an issue: GitHub.