Automattic/mongoose · error · MongooseError

Model.exists() no longer accepts a callback

Error message

Model.exists() no longer accepts a callback

What it means

Model.exists() was callback-capable in Mongoose 5/6 (exists(filter, options, callback)); in Mongoose 7 it returns a lean Query/promise only. Calling it with a function in the third argument position throws this MongooseError so legacy three-argument calls fail loudly instead of never invoking the callback.

Source

Thrown at lib/model.js:950

 *     await Character.deleteMany({});
 *     await Character.create({ name: 'Jean-Luc Picard' });
 *
 *     await Character.exists({ name: /picard/i }); // { _id: ... }
 *     await Character.exists({ name: /riker/i }); // null
 *
 * This function triggers the following middleware.
 *
 * - `findOne()`
 *
 * @param {object} filter
 * @param {object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())
 * @return {Query}
 */

Model.exists = function exists(filter, options) {
  _checkContext(this, 'exists');
  if (typeof arguments[2] === 'function') {
    throw new MongooseError('Model.exists() no longer accepts a callback');
  }

  const query = this.findOne(filter).
    select({ _id: 1 }).
    lean().
    setOptions(options);

  return query;
};

/**
 * Adds a discriminator type.
 *
 * #### Example:
 *
 *     function BaseSchema() {
 *       Schema.apply(this, arguments);
 *

View on GitHub (pinned to 49cdab0136)

Solutions

  1. Use await: const found = await Model.exists({ email }) — resolves to a lean { _id } document or null
  2. Wrap legacy callbacks at the boundary: Model.exists(f, o).then(r => cb(null, !!r), cb)
  3. Sweep for `.exists(` calls with a trailing function argument during the migration

Example fix

// before
User.exists({ email }, null, function (err, exists) {
  if (exists) return res.status(409).end();
  res.end();
});

// after
const exists = await User.exists({ email });
if (exists) return res.status(409).end();
res.end();
Defensive patterns

Strategy: validation

Validate before calling

// Normalize legacy exists() signatures before calling
function exists(model, filter, options) {
  if (typeof options === 'function' || typeof arguments[3] === 'function') {
    throw new Error('exists() is promise-based — await it instead of passing a callback');
  }
  return model.exists(filter, options);
}

Type guard

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

Try / catch

try {
  const found = await Model.exists({ email });
} catch (err) {
  if (err instanceof mongoose.MongooseError && /no longer accepts a callback/.test(err.message)) {
    // remove the trailing callback argument and await the query
  } else throw err;
}

Prevention

When it happens

Trigger: Model.exists({ email }, null, function (err, ok) { ... }) or Model.exists({ email }, {}, cb) on Mongoose 7+ — the classic duplicate-check route handler left un-migrated.

Common situations: Mongoose 6-to-7 upgrades; signup/endpoints checking for existing records; automated migrations that stripped most callbacks but missed the three-argument exists() form.

Related errors


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