{"record":{"id":"5c45816e183f7bd9","repo":"Automattic/mongoose","slug":"model-exists-no-longer-accepts-a-callback","errorCode":null,"errorMessage":"Model.exists() no longer accepts a callback","messagePattern":"Model\\.exists\\(\\) no longer accepts a callback","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/model.js","lineNumber":950,"sourceCode":" *     await Character.deleteMany({});\n *     await Character.create({ name: 'Jean-Luc Picard' });\n *\n *     await Character.exists({ name: /picard/i }); // { _id: ... }\n *     await Character.exists({ name: /riker/i }); // null\n *\n * This function triggers the following middleware.\n *\n * - `findOne()`\n *\n * @param {object} filter\n * @param {object} [options] optional see [`Query.prototype.setOptions()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions())\n * @return {Query}\n */\n\nModel.exists = function exists(filter, options) {\n  _checkContext(this, 'exists');\n  if (typeof arguments[2] === 'function') {\n    throw new MongooseError('Model.exists() no longer accepts a callback');\n  }\n\n  const query = this.findOne(filter).\n    select({ _id: 1 }).\n    lean().\n    setOptions(options);\n\n  return query;\n};\n\n/**\n * Adds a discriminator type.\n *\n * #### Example:\n *\n *     function BaseSchema() {\n *       Schema.apply(this, arguments);\n *","sourceCodeStart":932,"sourceCodeEnd":968,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/model.js#L932-L968","documentation":"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.","triggerScenarios":"Model.exists({ email }, null, function (err, ok) { ... }) or Model.exists({ email }, {}, cb) on Mongoose 7+ — the classic duplicate-check route handler left un-migrated.","commonSituations":"Mongoose 6-to-7 upgrades; signup/endpoints checking for existing records; automated migrations that stripped most callbacks but missed the three-argument exists() form.","solutions":["Use await: const found = await Model.exists({ email }) — resolves to a lean { _id } document or null","Wrap legacy callbacks at the boundary: Model.exists(f, o).then(r => cb(null, !!r), cb)","Sweep for `.exists(` calls with a trailing function argument during the migration"],"exampleFix":"// before\nUser.exists({ email }, null, function (err, exists) {\n  if (exists) return res.status(409).end();\n  res.end();\n});\n\n// after\nconst exists = await User.exists({ email });\nif (exists) return res.status(409).end();\nres.end();","handlingStrategy":"validation","validationCode":"// Normalize legacy exists() signatures before calling\nfunction exists(model, filter, options) {\n  if (typeof options === 'function' || typeof arguments[3] === 'function') {\n    throw new Error('exists() is promise-based — await it instead of passing a callback');\n  }\n  return model.exists(filter, options);\n}","typeGuard":"const isCallback = (x) => typeof x === 'function';","tryCatchPattern":"try {\n  const found = await Model.exists({ email });\n} catch (err) {\n  if (err instanceof mongoose.MongooseError && /no longer accepts a callback/.test(err.message)) {\n    // remove the trailing callback argument and await the query\n  } else throw err;\n}","preventionTips":["Use `await Model.exists(filter)` — it resolves to a lean { _id } doc or null","During Mongoose 7 upgrades, sweep every exists() call for trailing functions","Keep duplicate-check helpers promise-based and let route handlers await them"],"tags":["mongoose","exists","callback","promises","migration"],"backgroundTag":"callback-api-removed","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}