{"id":"b89587543a1068bb","repo":"mongodb/node-mongodb-native","slug":"cannot-call-abort-on-a-stream-twice","errorCode":null,"errorMessage":"Cannot call abort() on a stream twice","messagePattern":"Cannot call abort\\(\\) on a stream twice","errorType":"exception","errorClass":"MongoAPIError","httpStatus":null,"severity":"error","filePath":"src/gridfs/upload.ts","lineNumber":209,"sourceCode":"      return queueMicrotask(callback);\n    }\n    this.state.streamEnd = true;\n    writeRemnant(this, callback);\n  }\n\n  /**\n   * Places this write stream into an aborted state (all future writes fail)\n   * and deletes all chunks that have already been written.\n   */\n  async abort(): Promise<void> {\n    if (this.state.streamEnd) {\n      // TODO(NODE-3485): Replace with MongoGridFSStreamClosed\n      throw new MongoAPIError('Cannot abort a stream that has already completed');\n    }\n\n    if (this.state.aborted) {\n      // TODO(NODE-3485): Replace with MongoGridFSStreamClosed\n      throw new MongoAPIError('Cannot call abort() on a stream twice');\n    }\n\n    this.state.aborted = true;\n    const remainingTimeMS = this.timeoutContext?.getRemainingTimeMSOrThrow(\n      `Upload timed out after ${this.timeoutContext?.timeoutMS}ms`\n    );\n\n    await this.chunks.deleteMany({ files_id: this.id }, { timeoutMS: remainingTimeMS });\n  }\n}\n\nfunction handleError(stream: GridFSBucketWriteStream, error: Error, callback: Callback): void {\n  if (stream.state.errored) {\n    queueMicrotask(callback);\n    return;\n  }\n  stream.state.errored = true;\n  queueMicrotask(() => callback(error));","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/gridfs/upload.ts#L191-L227","documentation":"Thrown by GridFSBucketWriteStream.abort() when state.aborted is already true, i.e. abort() was invoked more than once on the same upload stream. After the first abort sets state.aborted = true and deletes written chunks, a second abort has nothing to do and is rejected as a MongoAPIError (NODE-3485 tracks a dedicated type).","triggerScenarios":"Calling uploadStream.abort() twice in sequence; concurrent abort() calls (e.g. from a timeout handler and an error handler firing near-simultaneously); retry logic that re-invokes abort on failure.","commonSituations":"Multiple cleanup paths (timeout + manual cancel); error handlers that call abort which a prior handler already triggered; promise wrappers that retry abort.","solutions":["Guard with your own boolean: if (!aborted) { aborted = true; await stream.abort(); }","Remove duplicate abort calls across event handlers and centralize cancellation.","Catch MongoAPIError around abort and ignore the double-abort case when idempotent cancellation is intended."],"exampleFix":"// before\nstream.on('error', () => stream.abort());\nsetTimeout(() => stream.abort(), 1000); // double-abort risk\n\n// after\nlet aborted = false;\nconst safeAbort = async () => {\n  if (aborted) return;\n  aborted = true;\n  await stream.abort();\n};\nstream.on('error', safeAbort);\nsetTimeout(safeAbort, 1000);","handlingStrategy":"validation","validationCode":"let aborted = false;\nasync function safeAbort() {\n  if (aborted) return;\n  aborted = true;\n  await stream.abort();\n}","typeGuard":null,"tryCatchPattern":"try { await stream.abort(); } catch (e) {\n  if (e instanceof MongoAPIError && /twice/.test(e.message)) return;\n  throw e;\n}","preventionTips":["Use a single guarded abort() wrapper","Avoid firing abort from multiple independent handlers","Make cancellation logic idempotent"],"tags":["gridfs","upload-stream","abort","double-call"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}