Automattic/mongoose · error · MongooseError

Model.prototype.save() no longer accepts a callback

Error message

Model.prototype.save() no longer accepts a callback

What it means

Mongoose 7 removed callback-style APIs: Model.prototype.save() accepts only an options object and returns a Promise. If save() is called with a function as its first or second argument (the legacy (callback) or (options, callback) forms), it throws this MongooseError so the never-invoked callback fails loudly instead of silently.

Source

Thrown at lib/model.js:639

 * @param {boolean} [options.validateModifiedOnly=false] if `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
 * @param {number|string} [options.w] set the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#w-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
 * @param {boolean} [options.j] set to true for MongoDB to wait until this `save()` has been [journaled before resolving the returned promise](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern)
 * @param {number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
 * @param {boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Restrictions-on-Field-Names)
 * @param {boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
 * @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
 * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
 * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
 * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
 * @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
 * @return {Promise}
 * @api public
 * @see middleware https://mongoosejs.com/docs/middleware.html
 */

Model.prototype.save = async function save(options) {
  if (typeof options === 'function' || typeof arguments[1] === 'function') {
    throw new MongooseError('Model.prototype.save() no longer accepts a callback');
  }

  let parallelSave;
  this.$op = 'save';

  if (this.$__.saving) {
    parallelSave = new ParallelSaveError(this);
  } else {
    this.$__.saving = true;
  }

  options = new SaveOptions(options);
  if (Object.hasOwn(options, 'session')) {
    this.$session(options.session);
  }
  if (this.$__.timestamps != null) {
    options.timestamps = this.$__.timestamps;
  }

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Replace callbacks with await inside try/catch: `try { const d = await doc.save(); } catch (err) { ... }`
  2. If a boundary must keep a callback API, promisify internally: doc.save(opts).then(r => cb(null, r), cb)
  3. Grep the codebase for `.save(function` and `.save(` with trailing callback args as part of the Mongoose 7 migration

Example fix

// before
doc.save(function (err, saved) {
  if (err) return next(err);
  res.json(saved);
});

// after
try {
  const saved = await doc.save();
  res.json(saved);
} catch (err) {
  next(err);
}
Defensive patterns

Strategy: validation

Validate before calling

// Reject legacy callback usage at a wrapper boundary
function assertNoCallback(options, arg1) {
  if (typeof options === 'function' || typeof arg1 === 'function') {
    throw new Error('save() is promise-based — await it instead of passing a callback');
  }
}

Type guard

const isCallback = (x) => typeof x === 'function';

Try / catch

try {
  await doc.save({ session });
} catch (err) {
  // handle validation / Mongo errors here; callbacks no longer exist
}

Prevention

When it happens

Trigger: doc.save(function (err) { ... }) or doc.save({ session }, function (err) { ... }) on Mongoose 7+, typically after upgrading from Mongoose 5/6 without migrating call sites.

Common situations: mongoose 6-to-7 (or 5-to-7) upgrades; legacy Express handlers written against callback docs; wrapper libraries that forward user-supplied callbacks; old tutorials and generated code.

Related errors


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