{"record":{"id":"92b46c31063acf5a","repo":"Automattic/mongoose","slug":"aggregate-has-empty-pipeline","errorCode":null,"errorMessage":"Aggregate has empty pipeline","messagePattern":"Aggregate has empty pipeline","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"warning","filePath":"lib/aggregate.js","lineNumber":804,"sourceCode":"/**\n * Execute the aggregation with explain\n *\n * #### Example:\n *\n *     Model.aggregate(..).explain()\n *\n * @param {'queryPlanner'|'executionStats'|'allPlansExecution'} [verbosity]\n * @return {Promise}\n */\n\nAggregate.prototype.explain = async function explain(verbosity) {\n  if (typeof verbosity === 'function' || typeof arguments[1] === 'function') {\n    throw new MongooseError('Aggregate.prototype.explain() no longer accepts a callback');\n  }\n  const model = this._model;\n\n  if (!this._pipeline.length) {\n    throw new MongooseError('Aggregate has empty pipeline');\n  }\n\n  prepareDiscriminatorPipeline(this._pipeline, this._model.schema);\n\n  const preFilter = buildMiddlewareFilter(this.options, 'pre');\n  const postFilter = buildMiddlewareFilter(this.options, 'post');\n\n  // Remove middleware option before passing to MongoDB\n  const options = this.options != null ? { ...this.options } : {};\n  delete options.middleware;\n\n  try {\n    await model.hooks.execPre('aggregate', this, [], { filter: preFilter });\n  } catch (error) {\n    return await model.hooks.execPost('aggregate', this, [null], { error, filter: postFilter });\n  }\n\n  const cursor = await model.collection.aggregate(this._pipeline, options);","sourceCodeStart":786,"sourceCodeEnd":822,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/aggregate.js#L786-L822","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst doc = await Model.findOneAndUpdate(filter, update, { new: true });\n\n// after\nconst doc = await Model.findOneAndUpdate(filter, update, { returnDocument: 'after' });","handlingStrategy":"validation","validationCode":"const AFTER = { returnDocument: 'after' };\n// one shared constant instead of inline { new: true }\nconst doc = await Model.findOneAndUpdate(filter, update, { ...AFTER, runValidators: true });","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["mongoose","deprecation","findoneandupdate","query-options","upgrade"],"backgroundTag":"deprecated-api-call","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}