Automattic/mongoose · error · MongooseError

Model.findByIdAndUpdate() no longer accepts a callback

Error message

Model.findByIdAndUpdate() no longer accepts a callback

What it means

Mongoose 7 removed callback support from all Model and Query APIs; findByIdAndUpdate() now returns a Query (thenable) only. The function inspects arguments[0] through arguments[3] and throws this MongooseError synchronously if any of them is a function. The check covers every position because a callback was previously allowed as the last argument (after id, update, options).

Source

Thrown at lib/model.js:2510

 * @param {object|string} [options.sort] if multiple docs are found by the conditions, sets the sort order to choose which doc to update.
 * @param {boolean} [options.runValidators] if true, runs [update validators](https://mongoosejs.com/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema
 * @param {boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created
 * @param {boolean} [options.includeResultMetadata] if true, returns the full [ModifyResult from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/7.0/interfaces/ModifyResult.html) rather than just the document
 * @param {boolean} [options.upsert=false] if true, and no documents found, insert a new document
 * @param {boolean} [options.new=false] if true, return the modified document rather than the original
 * @param {object|string} [options.select] sets the document fields to return.
 * @param {boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object.
 * @param {boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key
 * @return {Query}
 * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate()
 * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/
 * @api public
 */

Model.findByIdAndUpdate = function(id, update, options) {
  _checkContext(this, 'findByIdAndUpdate');
  if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function' || typeof arguments[3] === 'function') {
    throw new MongooseError('Model.findByIdAndUpdate() no longer accepts a callback');
  }

  // if a model is passed in instead of an id
  if (id instanceof Document) {
    id = id._doc._id;
  }

  return this.findOneAndUpdate.call(this, { _id: id }, update, options);
};

/**
 * Issue a MongoDB `findOneAndDelete()` command.
 *
 * Finds a matching document, removes it, and returns the found document (if any).
 *
 * This function triggers the following middleware.
 *
 * - `findOneAndDelete()`

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Remove the callback and use await: const doc = await Model.findByIdAndUpdate(id, update, { new: true });, with try/catch for the error path
  2. Or chain .then()/.catch() on the returned Query: Model.findByIdAndUpdate(id, update).then(doc => ...).catch(err => ...)
  3. Grep the codebase for callback patterns during upgrade: rg "findByIdAndUpdate\\([^)]*,\\s*(function|\\([^)]*\\)\\s*=>)" and migrate every hit (Mongoose 6 to 7 migration guide has a checklist)
  4. If an immediate rewrite is impossible, temporarily pin mongoose@6 and schedule the migration; do not shim callbacks by wrapping the returned Query
  5. If using TypeScript, rely on the Mongoose 7+ type definitions: callback overloads were deleted, so tsc flags these call sites at compile time

Example fix

// before (Mongoose 6)
User.findByIdAndUpdate(id, { name: 'a' }, { new: true }, (err, doc) => {
  if (err) return handleError(err);
  console.log(doc);
});

// after (Mongoose 7/8)
try {
  const doc = await User.findByIdAndUpdate(id, { name: 'a' }, { new: true });
  console.log(doc);
} catch (err) {
  handleError(err);
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard shared call sites / migration shims against callback args
function assertNoCallbacks(fnName, args) {
  const i = args.findIndex(a => typeof a === 'function');
  if (i !== -1) {
    throw new TypeError(`${fnName}: arg ${i + 1} is a function; use await (Mongoose 7+ is promise-only)`);
  }
}
// assertNoCallbacks('findByIdAndUpdate', [id, update, options]);

Try / catch

try {
  const doc = await Model.findByIdAndUpdate(id, update, { new: true });
} catch (err) {
  if (err instanceof mongoose.MongooseError && /no longer accepts a callback/.test(err.message)) {
    // legacy callback call site slipped through the migration
    throw new Error('Migrate this call site to await/then', { cause: err });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling Model.findByIdAndUpdate(id, update, options, callback) with Mongoose 6-style callback code, or Model.findByIdAndUpdate(id, update, callback) where the callback lands in the options slot (arguments[2]). Also triggered by argument-order bugs where a function is passed as id or update.

Common situations: Upgrading an app or a dependency from Mongoose 6 (or earlier) to Mongoose 7/8 without migrating call sites; copy-pasting code from old tutorials or Stack Overflow answers that use function (err, doc) {} style; shared middleware/helpers written for the callback API.

Related errors


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