{"record":{"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/dce7939f86fb283e167ad709955abedb7bf23124/src/gridfs/upload.ts#L191-L227","documentation":"GridFSBucketWriteStream.abort() (src/gridfs/upload.ts:207) throws MongoAPIError 'Cannot call abort() on a stream twice' if abort has already been invoked (state.aborted === true). Abort is a one-time operation that deletes already-written chunks; calling it again is a logic error.","triggerScenarios":"Calling uploadStream.abort() two or more times. Multiple cleanup handlers (e.g. signal listener + finally) both invoking abort on the same stream.","commonSituations":"An AbortSignal listener and a try/finally both calling abort. Retry/cancellation logic that does not track whether abort already fired.","solutions":["Track whether abort has been called with a boolean flag and skip subsequent calls.","Use a single owner for the abort decision (either the AbortSignal handler or the finally block, not both).","Catch this specific error and ignore it if defensive double-abort is unavoidable."],"exampleFix":"// before\nawait s.abort();\nawait s.abort(); // throws\n// after\nlet aborted = false;\nasync function safeAbort() { if (!aborted) { aborted = true; await s.abort(); } }\nawait safeAbort();\nawait safeAbort();","handlingStrategy":"validation","validationCode":"function onceAbort(stream) {\n  let called = false;\n  return async () => {\n    if (called) return;\n    called = true;\n    return stream.abort();\n  };\n}","typeGuard":"function isStreamAborted(stream: GridFSBucketWriteStream): boolean {\n  return (stream as any).state?.aborted === true;\n}","tryCatchPattern":"try {\n  await stream.abort();\n} catch (e) {\n  if (e instanceof MongoAPIError && /abort\\(\\) on a stream twice/.test(e.message)) {\n    // no-op: already aborted\n  } else throw e;\n}","preventionTips":["Wrap abort in a single-call guard so multiple handlers share one invocation.","Centralize cancellation in one owner (signal handler OR finally, not both).","Track aborted state explicitly in your upload orchestration."],"tags":["gridfs","stream","lifecycle"],"backgroundTag":null,"analyzedSha":"dce7939f86fb283e167ad709955abedb7bf23124","analyzedAt":"2026-08-11T04:54:53.215Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}