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

  1. Save the top-level parent: mutate the subdoc, then await parent.save() — Mongoose serializes the changed subdoc into the parent's update.
  2. If you only want the middleware side effects: await subdoc.save({ suppressWarning: true }) with a comment stating no DB write occurs.
  3. 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

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


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