Automattic/mongoose · error · MongooseError

Query.prototype.findOneAndReplace() no longer accepts a call

Error message

Query.prototype.findOneAndReplace() no longer accepts a callback

What it means

Query.prototype.findOneAndReplace(filter, replacement, options) throws when filter, replacement, or options is a function, or when a fifth positional argument (arguments[4]) is a function. Note the guard checks arguments[4] rather than arguments[3], so a callback passed as a fourth argument is not caught by this specific check — functions in the named parameter slots are what reliably trigger it. This is part of the Mongoose 7 removal of callback execution.

Source

Thrown at lib/query.js:3789

 * @param {boolean} [options.new=false] By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. If you set `new: true`, `findOneAndUpdate()` will instead give you the object after `update` was applied. **Deprecated:** Use `returnDocument: 'after'` instead of `new: true`, or `returnDocument: 'before'` instead of `new: false`.
 * @param {object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean()) and [the Mongoose lean tutorial](https://mongoosejs.com/docs/tutorials/lean.html).
 * @param {ClientSession} [options.session=null] The session associated with this query. See [transactions docs](https://mongoosejs.com/docs/transactions.html).
 * @param {boolean|'throw'} [options.strict] overwrites the schema's [strict mode option](https://mongoosejs.com/docs/guide.html#strict)
 * @param {boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set.
 * @param {boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. **Deprecated:** Use `returnDocument: 'after'` instead of `returnOriginal: false`, or `returnDocument: 'before'` instead of `returnOriginal: true`.
 * @param {'before'|'after'} [options.returnDocument='before'] Has two possible values, `'before'` and `'after'`. By default, it will return the document before the update was applied.
 * @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.requireFilter=false] If true, throws an error if the filter is empty (`{}`)
 * @return {Query} this
 * @api public
 */

Query.prototype.findOneAndReplace = function(filter, replacement, options) {
  if (typeof filter === 'function' ||
      typeof replacement === 'function' ||
      typeof options === 'function' ||
      typeof arguments[4] === 'function') {
    throw new MongooseError('Query.prototype.findOneAndReplace() no longer accepts a callback');
  }

  this.op = 'findOneAndReplace';
  this._validate();

  if (canMerge(filter)) {
    this.merge(filter);
  } else if (filter != null) {
    this.error(
      new ObjectParameterError(filter, 'filter', 'findOneAndReplace')
    );
  }

  if (replacement != null) {
    this._mergeUpdate(replacement);
  }

  options = options || {};

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Use await: const doc = await Model.findOneAndReplace(filter, replacement, { returnDocument: 'after' })
  2. Wrap in try/catch; the promise resolves null when no document matched
  3. Strip callbacks from all findOneAndReplace call sites when migrating to mongoose 7+

Example fix

// before
Model.findOneAndReplace({ key: 'theme' }, { key: 'theme', color: 'blue' }, (err, doc) => {
  if (err) return next(err);
  res.json(doc);
});

// after
try {
  const doc = await Model.findOneAndReplace(
    { key: 'theme' },
    { key: 'theme', color: 'blue' },
    { returnDocument: 'after' }
  );
  res.json(doc);
} catch (err) {
  next(err);
}
Defensive patterns

Strategy: validation

Validate before calling

function findOneAndReplaceSafe(model, filter, replacement, options) {
  if ([filter, replacement, options].some(v => typeof v === 'function')) {
    throw new Error('findOneAndReplace() is promise-only in Mongoose 7+');
  }
  return model.findOneAndReplace(filter, replacement, options);
}

Type guard

const isLegacyCallback = (v) => typeof v === 'function';

Try / catch

try {
  const doc = await Model.findOneAndReplace(filter, replacement, { returnDocument: 'after' });
} catch (err) {
  if (err?.message?.includes('no longer accepts a callback')) {
    // fix the findOneAndReplace call site that still passes a callback
  }
  throw err;
}

Prevention

When it happens

Trigger: Model.findOneAndReplace(filter, replacement, cb) with the callback in the options slot; .findOneAndReplace(filter, cb) with the callback in the replacement slot; legacy code passing a fifth-argument callback.

Common situations: Document-replacement flows (config documents, upsert-by-replace) written for Mongoose 6; major upgrades; legacy wrappers that append callbacks positionally.

Related errors


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