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 stillView on GitHub (pinned to 49cdab0136)
Solutions
- Drop the callback and await: await aggCursor.eachAsync(fn, { parallel: 4 })
- Keep error handling in a try/catch around the await
- 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
- Remember the exact trigger: only the third argument (fn, opts, cb) throws; a (fn, cb) call silently drops the callback
- Standardize on await eachAsync(fn, opts) plus try/catch for errors
- Cover cursor code paths in migration tests so unmigrated callbacks fail in CI, not production
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
- AggregationCursor.prototype.close() no longer accepts a call
- AggregationCursor.prototype.next() no longer accepts a callb
- QueryCursor.prototype.eachAsync() no longer accepts a callba
- QueryCursor.prototype.close() no longer accepts a callback
- QueryCursor.prototype.next() no longer accepts a callback
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/499deeaf2b0f62b9.
Report an issue: GitHub.