Automattic/mongoose · error · MongooseError

Document.prototype.populate() no longer accepts a callback

Error message

Document.prototype.populate() no longer accepts a callback

What it means

Mongoose 7 removed callback support from all APIs: Document.prototype.populate() is now async and returns a Promise. If the last argument passed is a function, Mongoose treats it as a legacy callback and throws immediately instead of silently never calling it. The guard exists to break old callback-style code loudly during the Mongoose 6 to 7+ migration.

Source

Thrown at lib/document.js:4886

 * @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
 * @param {object} [options.options=null] Additional options like `limit` and `lean`.
 * @param {boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
 * @param {boolean} [options.ordered=false] Set to `true` to execute any populate queries one at a time, as opposed to in parallel. We recommend setting this option to `true` if using transactions, especially if also populating multiple paths or paths with multiple models. MongoDB server does **not** support multiple operations in parallel on a single transaction.
 * @param {Function} [callback] Callback
 * @see population https://mongoosejs.com/docs/populate.html
 * @see Query#select https://mongoosejs.com/docs/api/query.html#Query.prototype.select()
 * @see Model.populate https://mongoosejs.com/docs/api/model.html#Model.populate()
 * @memberOf Document
 * @instance
 * @return {Promise|null} Returns a Promise if no `callback` is given.
 * @api public
 */

Document.prototype.populate = async function populate() {
  const pop = {};
  const args = [...arguments];
  if (typeof args[args.length - 1] === 'function') {
    throw new MongooseError('Document.prototype.populate() no longer accepts a callback');
  }

  if (args.length !== 0) {
    // use hash to remove duplicate paths
    const res = utils.populate.apply(null, args);
    for (const populateOptions of res) {
      pop[populateOptions.path] = populateOptions;
    }
  }

  const paths = utils.object.vals(pop);

  let topLevelModel = this.constructor;
  if (this.$__isNested) {
    topLevelModel = this.$__[scopeSymbol].constructor;
    const nestedPath = this.$__.nestedPath;
    paths.forEach(function(populateOptions) {
      populateOptions.path = nestedPath + '.' + populateOptions.path;

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Remove the callback and await the call: const doc = await doc.populate('path').
  2. Use promise chaining: doc.populate('path').then(d => ...).catch(handleError).
  3. If a callback interface must be kept, wrap it yourself: doc.populate('path').then(d => cb(null, d), cb).
  4. Before upgrading major versions, grep for populate( calls whose last argument is a function or arrow, and migrate them all at once using the mongoose migration guide.

Example fix

// before
doc.populate('author', (err, d) => { if (err) throw err; console.log(d.author.name); });

// after
const d = await doc.populate('author');
console.log(d.author.name);
Defensive patterns

Strategy: validation

Validate before calling

// reject callback-style populate before calling mongoose
function safePopulate(doc, ...args) {
  if (typeof args[args.length - 1] === 'function') {
    throw new TypeError('populate() is promise-only; remove the callback argument');
  }
  return doc.populate(...args);
}

Try / catch

try {
  await doc.populate('author');
} catch (err) {
  if (err instanceof mongoose.MongooseError && err.message.includes('no longer accepts a callback')) {
    // a callback leaked into the call: remove it at the call site and retry promise-style
    return doc.populate('author');
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling doc.populate('path', (err, d) => {...}), doc.populate({ path: 'path' }, cb), or doc.populate(cb) with no paths - any invocation where the last argument is a function triggers the throw inside the async populate() wrapper.

Common situations: Upgrading mongoose from ^6 to ^7/^8 with legacy callback code; snippets copied from pre-2022 tutorials or Stack Overflow answers; wrapper layers that append a callback for backward API compatibility.

Related errors


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