{"record":{"id":"b2feaf98cf2e15aa","repo":"Automattic/mongoose","slug":"cannot-call-next-on-a-closed-cursor","errorCode":null,"errorMessage":"Cannot call `next()` on a closed cursor","messagePattern":"Cannot call `next\\(\\)` on a closed cursor","errorType":"exception","errorClass":"MongooseError","httpStatus":null,"severity":"error","filePath":"lib/cursor/queryCursor.js","lineNumber":312,"sourceCode":"  });\n  return this;\n};\n\n/**\n * Get the next document from this cursor. Will return `null` when there are\n * no documents left.\n *\n * @return {Promise}\n * @api public\n * @method next\n */\n\nQueryCursor.prototype.next = async function next() {\n  if (typeof arguments[0] === 'function') {\n    throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');\n  }\n  if (this._closed) {\n    throw new MongooseError('Cannot call `next()` on a closed cursor');\n  }\n  const _this = this;\n  return cursorNextChannel.trace(function maybeTracedQueryCursorNext() {\n    return new Promise((resolve, reject) => {\n      _next(_this, function(error, doc) {\n        if (error) {\n          return reject(error);\n        }\n        resolve(doc);\n      });\n    });\n  }, () => ({\n    operation: _this.query.op || 'find',\n    collection: _this.query.mongooseCollection.name,\n    database: _this.model.db?.name,\n    serverAddress: _this.model.db?.host,\n    serverPort: _this.model.db?.port,\n    batchSize: _this.options.batchSize || _this.query.options?.batchSize,","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/cursor/queryCursor.js#L294-L330","documentation":"QueryCursor sets _closed = true once close() succeeds. A subsequent next() throws this error because the underlying driver cursor is destroyed; Mongoose fails fast instead of hanging or serving stale data.","triggerScenarios":"const cur = Model.find().cursor(); await cur.close(); await cur.next(); — reading after close; also a race where one consumer closes the cursor while another keeps calling next().","commonSituations":"Early-exit loops that close the cursor and then fall through to one more next(); concurrent tasks sharing a single cursor; cleanup or timeout handlers racing with iteration.","solutions":["Stop iterating immediately after close() — return or break out of the read loop","If more documents are needed later, create a fresh cursor by re-running the query: Model.find().cursor()","Prefer letting the cursor drain naturally (next() resolves null) and only close() for early exit"],"exampleFix":"// before\nconst docs = [];\nfor (let i = 0; i < 3; i++) docs.push(await cursor.next());\nawait cursor.close();\nconst extra = await cursor.next(); // throws\n\n// after\nlet doc; let n = 0;\nwhile ((doc = await cursor.next()) !== null && n < 3) { docs.push(doc); n++; }\nawait cursor.close();\n// need more later? create a fresh cursor\nconst fresh = Model.find().cursor();","handlingStrategy":"validation","validationCode":"let closed = false;\nasync function closeCursor() {\n  closed = true;\n  await cursor.close();\n}\nasync function safeNext() {\n  if (closed) return null; // treat as exhausted instead of calling next()\n  return cursor.next();\n}","typeGuard":null,"tryCatchPattern":"try {\n  doc = await cursor.next();\n} catch (err) {\n  if (/closed cursor/.test(err.message)) return null; // already closed: treat as end of stream\n  throw err;\n}","preventionTips":["Give the cursor exactly one owner loop; never share it across concurrent consumers","Track a closed flag in app code the moment close() is called","Prefer eachAsync() for bounded processing — it handles exhaustion internally"],"tags":["query-cursor","cursor-lifecycle","close","mongoose"],"backgroundTag":"cursor-already-closed","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}