Automattic/mongoose · warning · MongooseError
Aggregate.prototype.exec() no longer accepts a callback
Error message
Aggregate.prototype.exec() no longer accepts a callback
What it means
Subdocuments (items of a document array or single nested docs) share the Document prototype, so subdoc.save() exists — but it does NOT write to MongoDB. It only executes the save middleware chain on the subdoc; persistence happens when the top-level parent document is saved. Mongoose warns on every call to make this explicit, with an opt-out for apps that knowingly use it just to run hooks.
Source
Thrown at lib/aggregate.js:1069
return this._pipeline;
};
/**
* Executes the aggregate pipeline on the currently bound Model.
*
* #### Example:
* const result = await aggregate.exec();
*
* @return {Promise}
* @api public
*/
Aggregate.prototype.exec = async function exec() {
if (!this._model && !this._connection) {
throw new MongooseError('Aggregate not bound to any Model');
}
if (typeof arguments[0] === 'function') {
throw new MongooseError('Aggregate.prototype.exec() no longer accepts a callback');
}
if (this._connection) {
if (!this._pipeline.length) {
throw new MongooseError('Aggregate has empty pipeline');
}
this._optionsForExec();
const _this = this;
return traceAggregate(async function maybeTracedConnectionAggregate() {
const cursor = await _this._connection.client.db().aggregate(_this._pipeline, _this.options);
return await cursor.toArray();
}, () => ({
operation: 'aggregate',
database: _this._connection.name,
serverAddress: _this._connection.host,
serverPort: _this._connection.port,View on GitHub (pinned to 49cdab0136)
Solutions
- Save the top-level parent: mutate the subdoc, then await parent.save() — Mongoose serializes the changed subdoc into the parent's update.
- If you only want the middleware side effects: await subdoc.save({ suppressWarning: true }) with a comment stating no DB write occurs.
- For bulk subdoc updates, consider update operators on the parent: Model.updateOne({ 'items._id': id }, { $set: { 'items.$.qty': 2 } }) to avoid loading/saving the whole document.
Example fix
// before const sub = parent.children.id(itemId); sub.qty = 2; await sub.save(); // does NOT persist // after const sub = parent.children.id(itemId); sub.qty = 2; await parent.save(); // persists the subdoc change
Defensive patterns
Strategy: validation
Validate before calling
async function saveSubdoc(parent, subdoc) {
// persistence always flows through the parent document
subdoc.markModified?.(); // ensure tracked if mutated via plain object
await parent.save();
} Type guard
const isSubdocument = (doc) => doc != null && doc.$isSingleNested === true || doc?.ownerDocument?.() != null;
Prevention
- Treat save() as a root-document-only operation; subdocs persist via parent.save().
- Wrap subdoc updates in helpers that always receive the parent, so calling sub.save() directly is impossible.
- If you intentionally run only middleware on a subdoc, call save({ suppressWarning: true }) and comment why.
When it happens
Trigger: const sub = parent.children.id(id); await sub.save(); expecting the subdoc change to be persisted; looping over doc.items and calling item.save(); calling save() on a single nested subdoc (subdoc = doc.nested) to persist a modification.
Common situations: Developers assuming uniform Repository-style save() on any entity; refactors that extracted subdoc handling into functions which save the subdoc directly; bugs where changes are lost because only middleware ran; test suites passing because hooks fired while the DB never changed.
Related errors
- Infinite subdocument loop: subdoc with _id ${doc._id} is a p
- Invalid arguments
- Expected path "${path}" to be populated
- For your own good, using `document.save()` to update an arra
- 2nd argument to `Model` constructor must be a POJO or string
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/8d2459e7c2b45088.
Report an issue: GitHub.