{"record":{"id":"07c4953a9d11ecf7","repo":"Automattic/mongoose","slug":"query-must-have-op-before-executing","errorCode":null,"errorMessage":"Query must have `op` before executing","messagePattern":"Query must have `op` before executing","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/query.js","lineNumber":4755,"sourceCode":" *     const promise = query.exec('update');\n *\n * @param {string|Function} [operation]\n * @return {Promise}\n * @api public\n */\n\nQuery.prototype.exec = async function exec(op) {\n  if (typeof op === 'function' || (arguments.length >= 2 && typeof arguments[1] === 'function')) {\n    throw new MongooseError('Query.prototype.exec() no longer accepts a callback');\n  }\n\n  this._validateOp();\n  if (typeof op === 'string') {\n    this.op = op;\n  }\n\n  if (this.op == null) {\n    throw new MongooseError('Query must have `op` before executing');\n  }\n  if (this.model == null) {\n    throw new MongooseError('Query must have an associated model before executing');\n  }\n\n  const thunk = opToThunk.get(this.op);\n  if (!thunk) {\n    throw new MongooseError('Query has invalid `op`: \"' + this.op + '\"');\n  }\n\n  if (this.options?.sort && typeof this.options.sort === 'object' && Object.hasOwn(this.options.sort, '')) {\n    throw new MongooseError('Invalid field \"\" passed to sort()');\n  }\n\n  if (this._execCount > 0) {\n    let str = this.toString();\n    if (str.length > 60) {\n      str = str.slice(0, 60) + '...';","sourceCodeStart":4737,"sourceCodeEnd":4773,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/query.js#L4737-L4773","documentation":"Query.prototype.exec() requires that the query already knows which operation to run; `this.op` is set by find(), findOne(), updateOne(), countDocuments(), etc. If exec() runs on a query with no operation, Mongoose throws this error because there is nothing meaningful to send to the server.","triggerScenarios":"`Model.where('name', 'x').exec()` (where() only adds a condition, it does not set an op); `new Model.Query().exec()`; exec('') with an empty string; building conditions via query helpers and forgetting the terminal find/findOne call.","commonSituations":"Query-builder helpers that chain where()/sort()/limit() and accidentally return before calling find(); dynamically skipping the operation-setting step because of a falsy branch; copy-paste that drops the `.find()`.","solutions":["Add the operation before exec: `Model.find({ name: 'x' }).exec()` or `Model.where('name', 'x').find().exec()`.","Or pass the op string to exec if the query has conditions but no op: `query.exec('find')`.","Audit builder functions to guarantee every code path ends in find()/findOne()/updateOne()/etc."],"exampleFix":"// before\nconst docs = await Model.where('age').gte(18).exec(); // throws: Query must have `op` before executing\n\n// after\nconst docs = await Model.where('age').gte(18).find().exec();","handlingStrategy":"validation","validationCode":"function execQuery(query) {\n  if (query.op == null) query.find(); // or throw: conditions without an operation\n  return query.exec();\n}","typeGuard":"const queryHasOp = (q) => q.op != null;","tryCatchPattern":"try { await q.exec(); } catch (err) { if (err instanceof mongoose.Error && /must have `op`/.test(err.message)) { q.find(); return q.exec(); } throw err; }","preventionTips":["End every builder chain with find()/findOne()/updateOne().","Do not treat where()/sort()/limit() as executable terminals.","Unit-test builder helpers to assert q.op is set before exec."],"tags":["mongoose","query","exec","api-misuse"],"backgroundTag":"mongoose-missing-query-op","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}