{"id":"222a5a6dd394a296","repo":"mongodb/node-mongodb-native","slug":"cannot-abort-a-stream-that-has-already-completed","errorCode":null,"errorMessage":"Cannot abort a stream that has already completed","messagePattern":"Cannot abort a stream that has already completed","errorType":"exception","errorClass":"MongoAPIError","httpStatus":null,"severity":"error","filePath":"src/gridfs/upload.ts","lineNumber":204,"sourceCode":"  }\n\n  /** @internal */\n  override _final(callback: (error?: Error | null) => void): void {\n    if (this.state.streamEnd) {\n      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) {","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/mongodb/node-mongodb-native/blob/3366c21a6311e02f1be91da982f9b93d3cce99a0/src/gridfs/upload.ts#L186-L222","documentation":"Thrown by GridFSBucketWriteStream.abort() when the upload stream has already reached its end state (state.streamEnd === true), meaning _final has run or the stream completed/errored. Abort is only meaningful mid-upload to discard partial chunks; once the stream is done, aborting is invalid. Classified as MongoAPIError (TODO NODE-3485 plans a MongoGridFSStreamClosed type).","triggerScenarios":"Awaiting uploadStream.end() (or finishing the stream) and then calling uploadStream.abort(); calling abort inside a 'finish' or 'error' event handler; calling abort after an error already ended the stream.","commonSituations":"Cleanup code in a finally block that runs abort unconditionally; stream pipelines that call abort on close events; abort invoked from both success and error paths.","solutions":["Track upload completion yourself and only call abort while the stream is still writable: if (!stream.destroyed && !stream.writableEnded) await stream.abort();","Move abort() calls to error-handling branches that fire before end() completes.","Guard cleanup with a flag set in the 'finish' and 'error' listeners."],"exampleFix":"// before\nstream.end();\nawait stream.abort(); // throws\n\n// after\nlet finished = false;\nstream.on('finish', () => { finished = true; });\nstream.on('error', () => { finished = true; });\nstream.end();\nif (!finished) await stream.abort();","handlingStrategy":"validation","validationCode":"let ended = false;\nstream.on('finish', () => { ended = true; });\nstream.on('error', () => { ended = true; });\nasync function safeAbort() {\n  if (!ended) await stream.abort();\n}","typeGuard":"function streamEnded(s): boolean {\n  return s.destroyed || s.writableEnded;\n}","tryCatchPattern":"try { await stream.abort(); } catch (e) {\n  if (e instanceof MongoAPIError && /already completed/.test(e.message)) return;\n  throw e;\n}","preventionTips":["Track stream completion in finish/error handlers","Never call abort from inside finish/error listeners","Centralize abort into a single guarded function"],"tags":["gridfs","upload-stream","abort"],"analyzedSha":"3366c21a6311e02f1be91da982f9b93d3cce99a0","analyzedAt":"2026-08-04T13:40:15.335Z","schemaVersion":2}