Automattic/mongoose · error · MongooseError

AggregationCursor.prototype.eachAsync() no longer accepts a

Error message

AggregationCursor.prototype.eachAsync() no longer accepts a callback

What it means

eachAsync() on an aggregation cursor no longer accepts a completion callback as its third argument in Mongoose 7+. Passing (fn, opts, cb) throws immediately; the method returns a Promise that settles when iteration finishes. Note the guard checks arguments[2] specifically — passing (fn, cb) does not throw, but that callback is silently ignored.

Source

Thrown at lib/cursor/aggregationCursor.js:338

/**
 * Execute `fn` for every document in the cursor. If `fn` returns a promise,
 * will wait for the promise to resolve before iterating on to the next one.
 * Returns a promise that resolves when done.
 *
 * @param {Function} fn
 * @param {object} [options]
 * @param {number} [options.parallel] the number of promises to execute in parallel. Defaults to 1.
 * @param {number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
 * @param {boolean} [options.continueOnError=false] if true, `eachAsync()` iterates through all docs even if `fn` throws an error. If false, `eachAsync()` throws an error immediately if the given function `fn()` throws an error.
 * @return {Promise}
 * @api public
 * @method eachAsync
 */

AggregationCursor.prototype.eachAsync = function(fn, opts) {
  if (typeof arguments[2] === 'function') {
    throw new MongooseError('AggregationCursor.prototype.eachAsync() no longer accepts a callback');
  }
  const _this = this;
  if (typeof opts === 'function') {
    opts = {};
  }
  opts = opts || {};

  return eachAsync(function(cb) { return _next(_this, cb); }, fn, opts);
};

/**
 * Returns an asyncIterator for use with [`for/await/of` loops](https://thecodebarbarian.com/getting-started-with-async-iterators-in-node-js)
 * You do not need to call this function explicitly, the JavaScript runtime
 * will call it for you.
 *
 * #### Example:
 *
 *     // Async iterator without explicitly calling `cursor()`. Mongoose still

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Drop the callback and await: await aggCursor.eachAsync(fn, { parallel: 4 })
  2. Keep error handling in a try/catch around the await
  3. If a callback interface must be preserved outward, wrap: eachAsync(fn, opts).then(() => cb(null), cb)

Example fix

// before
aggCursor.eachAsync(doc => save(doc), { parallel: 4 }, function (err) {
  if (err) return done(err);
  done();
});

// after
try {
  await aggCursor.eachAsync(doc => save(doc), { parallel: 4 });
  done();
} catch (err) {
  done(err);
}
Defensive patterns

Strategy: validation

Validate before calling

function runEachAsync(cursor, fn, opts, cb) {
  if (typeof cb === 'function') {
    // legacy 3-arg call: convert to promise, keep behavior explicit
    return cursor.eachAsync(fn, opts).then(() => cb(null), cb);
  }
  return cursor.eachAsync(fn, opts);
}

Prevention

When it happens

Trigger: aggCursor.eachAsync(fn, {}, cb) or aggCursor.eachAsync(fn, { batchSize: 10 }, function (err) {...}) — a function in the third argument slot.

Common situations: Migrating ETL scripts that used eachAsync with callbacks; combining options like parallel/batchSize with a legacy completion handler.

Related errors


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