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
- Remove the callback and await the call: const doc = await doc.populate('path').
- Use promise chaining: doc.populate('path').then(d => ...).catch(handleError).
- If a callback interface must be kept, wrap it yourself: doc.populate('path').then(d => cb(null, d), cb).
- 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
- Standardize on async/await for all mongoose calls; add a lint rule or codemod flagging callback-style mongoose usage before major upgrades.
- When upgrading mongoose majors, run the official migration guide checklist and grep for ', cb)' and ', function' near mongoose calls.
- Cover populate flows with integration tests so signature regressions fail in CI, not production.
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
- Model.init() no longer accepts a callback
- Model.createCollection() no longer accepts a callback
- Model.syncIndexes() no longer accepts a callback
- Model.cleanIndexes() no longer accepts a callback
- Model.listIndexes() no longer accepts a callback
AI-assisted analysis of Automattic/mongoose@49cdab0136 (2026-08-21).
Data as JSON: /api/errors/6b4c96c9bd9994e4.
Report an issue: GitHub.