{"record":{"id":"f06d11b9cb6036c1","repo":"Automattic/mongoose","slug":"options-must-be-an-object-got-options","errorCode":null,"errorMessage":"Options must be an object, got \"${options}\"","messagePattern":"Options must be an object, got \"(.+?)\"","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/query.js","lineNumber":1743,"sourceCode":" */\n\nQuery.prototype.setOptions = function(options, overwrite) {\n  // overwrite is only for internal use\n  if (overwrite) {\n    // ensure that _mongooseOptions & options are two different objects\n    this._mongooseOptions = (options && clone(options)) || {};\n    this.options = options || {};\n\n    if ('populate' in options) {\n      this.populate(this._mongooseOptions);\n    }\n    return this;\n  }\n  if (options == null) {\n    return this;\n  }\n  if (typeof options !== 'object') {\n    throw new MongooseError('Options must be an object, got \"' + options + '\"');\n  }\n\n  options = Object.assign({}, options);\n\n  if (Array.isArray(options.populate)) {\n    const populate = options.populate;\n    delete options.populate;\n    const _numPopulate = populate.length;\n    for (let i = 0; i < _numPopulate; ++i) {\n      this.populate(populate[i]);\n    }\n  }\n\n  if ('cloneUpdate' in options) {\n    this._mongooseOptions.cloneUpdate = options.cloneUpdate;\n    delete options.cloneUpdate;\n  }\n  if ('defaults' in options) {","sourceCodeStart":1725,"sourceCodeEnd":1761,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/query.js#L1725-L1761","documentation":"Query.prototype.setOptions() requires its options argument to be an object; null/undefined return early, and any other primitive (string, number, boolean) throws a MongooseError with the offending value interpolated into the message. Because every query helper that takes an options parameter (findOne, countDocuments, deleteOne, findOneAndUpdate, etc.) delegates to setOptions(), the error usually surfaces from the outer method call rather than an explicit setOptions() call.","triggerScenarios":".findOne(filter, projection, 'lean') instead of { lean: true }; .countDocuments({}, 10); .deleteOne({}, true); .setOptions('limit=5'); passing an unparsed JSON string pulled from an env var, cache, or message queue.","commonSituations":"Options serialized as strings (env vars, Redis, job queues) that were never JSON.parsed; argument positions shifted after removing a legacy callback during a Mongoose 7 upgrade; passing a URL query string instead of a parsed object.","solutions":["Pass a plain object: .findOne(filter, projection, { lean: true })","JSON.parse() any serialized options string before passing it","Re-check argument order — a primitive in the options slot usually means misaligned arguments (frequently a leftover callback or an extra leading parameter)"],"exampleFix":"// before\nconst doc = await Model.findOne({}, null, process.env.FIND_OPTS); // FIND_OPTS is the string '{\"lean\":true}'\n\n// after\nconst doc = await Model.findOne({}, null, JSON.parse(process.env.FIND_OPTS));","handlingStrategy":"type-guard","validationCode":"function toOptions(raw) {\n  if (raw == null) return undefined;\n  if (typeof raw === 'string') {\n    try { return JSON.parse(raw); } catch { return undefined; }\n  }\n  return typeof raw === 'object' ? raw : undefined;\n}\nconst doc = await Model.findOne({}, null, toOptions(maybeOptions));","typeGuard":"const isOptionsObject = (v) => v == null || (typeof v === 'object' && v !== null);","tryCatchPattern":null,"preventionTips":["Always pass options as a plain object literal","JSON.parse strings coming from env vars, caches, or queues before use","In TypeScript, type the options slot as a specific options interface, not any"],"tags":["mongoose","query","setoptions","options","invalid-argument"],"backgroundTag":"invalid-options-argument","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}