{"record":{"id":"0bd12bab595f93bd","repo":"Automattic/mongoose","slug":"batchsize-must-be-a-number","errorCode":null,"errorMessage":"batchSize must be a number","messagePattern":"batchSize must be a number","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"lib/helpers/cursor/eachAsync.js","lineNumber":50,"sourceCode":"  const enqueue = asyncQueue();\n\n  let aborted = false;\n\n  return new Promise((resolve, reject) => {\n    if (signal != null) {\n      if (signal.aborted) {\n        return resolve(null);\n      }\n\n      signal.addEventListener('abort', () => {\n        aborted = true;\n        return resolve(null);\n      }, { once: true });\n    }\n\n    if (batchSize != null) {\n      if (typeof batchSize !== 'number') {\n        throw new TypeError('batchSize must be a number');\n      } else if (!Number.isInteger(batchSize)) {\n        throw new TypeError('batchSize must be an integer');\n      } else if (batchSize < 1) {\n        throw new TypeError('batchSize must be at least 1');\n      }\n    }\n\n    iterate((err, res) => {\n      if (err != null) {\n        return reject(err);\n      }\n      resolve(res);\n    });\n  });\n\n  function iterate(finalCallback) {\n    let handleResultsInProgress = 0;\n    let currentDocumentIndex = 0;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/Automattic/mongoose/blob/49cdab01366679723b487ecb754b38570f783289/lib/helpers/cursor/eachAsync.js#L32-L68","documentation":"eachAsync on a query or aggregation cursor accepts an options object whose batchSize controls how many documents are fetched per batch. The helper validates batchSize eagerly: it must be a JavaScript number. Passing '100' (a string from an env var or query param) or any non-number throws TypeError before iteration starts; the check only runs when batchSize is not null/undefined.","triggerScenarios":"cursor.eachAsync(fn, { batchSize: '100' }); batchSize read from process.env.BATCH_SIZE or a URL query parameter without conversion; a shared utility forwarding caller-supplied strings into eachAsync.","commonSituations":"Config-driven batch sizes in ETL/backfill scripts; API handlers forwarding query-string options directly into cursor options; defaults that accidentally set batchSize to a non-number.","solutions":["Coerce before passing: batchSize: Number(process.env.BATCH_SIZE).","Parse query params explicitly: const batchSize = parseInt(req.query.batchSize, 10).","Omit batchSize entirely (pass undefined) when it is not configured - the validation skips null/undefined."],"exampleFix":"// before\nconst batchSize = process.env.BATCH_SIZE; // string\nawait Model.find().cursor().eachAsync(fn, { batchSize }); // TypeError\n\n// after\nconst batchSize = process.env.BATCH_SIZE != null ? Number(process.env.BATCH_SIZE) : undefined;\nawait Model.find().cursor().eachAsync(fn, { batchSize });","handlingStrategy":"validation","validationCode":"function toBatchSize(v) {\n  if (v == null) return undefined;\n  const n = Number(v);\n  if (!Number.isInteger(n) || n < 1) {\n    throw new TypeError(`invalid batchSize: ${v}`);\n  }\n  return n;\n}\nawait cursor.eachAsync(fn, { batchSize: toBatchSize(rawValue) });","typeGuard":"function isValidBatchSize(v) {\n  return typeof v === 'number' && Number.isInteger(v) && v >= 1;\n}","tryCatchPattern":null,"preventionTips":["Convert env vars and query-string inputs to numbers at the boundary (Number(), zod, joi).","Validate cursor option objects once in a shared helper instead of at every call site.","Treat null/undefined as unset rather than 0."],"tags":["mongoose","cursor","eachasync","batch-size","typeerror","env-var"],"backgroundTag":"invalid-batch-size","analyzedSha":"49cdab01366679723b487ecb754b38570f783289","analyzedAt":"2026-08-21T22:54:00.882Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}