{"record":{"id":"567d62d157b389eb","repo":"remix-run/react-router","slug":"cannot-write-to-a-destroyed-or-ended-writable-stre","errorCode":null,"errorMessage":"Cannot write to a destroyed or ended writable stream","messagePattern":"Cannot write to a destroyed or ended writable stream","errorType":"exception","errorClass":"WritableClosedError","httpStatus":null,"severity":"warning","filePath":"packages/react-router-node/stream.ts","lineNumber":135,"sourceCode":"  function onClose() {\n    reject(new WritableClosedError(\"Writable closed before stream finished\"));\n  }\n\n  writable.once(\"error\", onError);\n  writable.once(\"close\", onClose);\n\n  return {\n    cleanup,\n    race<T>(promise: Promise<T>) {\n      return Promise.race([promise, writableErrorPromise]);\n    },\n    throwIfClosed() {\n      if (writableError) {\n        throw writableError;\n      }\n\n      if (writable.destroyed || writable.writableEnded) {\n        throw new WritableClosedError(\n          \"Cannot write to a destroyed or ended writable stream\",\n        );\n      }\n    },\n  };\n}\n\nfunction waitForDrain(\n  writable: Writable,\n  writableError: WritableErrorMonitor,\n): Promise<void> {\n  let cleanup = () => {};\n  let drainPromise = new Promise<void>((resolve) => {\n    function onDrain() {\n      cleanup();\n      resolve();\n    }\n","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/remix-run/react-router/blob/917e6fdc0f8beb141e3380e83e27634d7be84e1d/packages/react-router-node/stream.ts#L117-L153","documentation":"@react-router/node's writeReadableStreamToWritable pumps a web ReadableStream into a Node Writable, guarded by monitorWritableError(). Before every read/write it calls throwIfClosed(), which rethrows a captured stream error, or throws this error when the writable is already destroyed or writableEnded. This turns silent write-after-end behavior into an explicit failure so the pump can cancel the source stream and clean up instead of writing into a dead response.","triggerScenarios":"The HTTP client disconnects (or a proxy closes the connection) mid-SSR-stream while chunks are still being pumped; a custom adapter calls res.end() or the response finished before the stream completed; backpressure stall followed by close, then the next write attempt hits the guard.","commonSituations":"Streaming SSR with @react-router/express or a custom Node adapter; users pressing Stop/refresh during a slow streamed render; upstream proxies timing out; manually ending the response around the adapter call.","solutions":["Treat it as a disconnect signal: catch it, cancel/abort the render source, and let the response die — do not retry","In custom adapters, listen for 'close' on the response and abort the request/render so no further writes are attempted","Remove any manual res.end() before writeReadableStreamToWritable has finished"],"exampleFix":"// before — custom Node adapter\nres.end(); // ended early (client gone or manual flush)\nawait writeReadableStreamToWritable(webStream, res); // throws\n\n// after\nres.on(\"close\", () => abortController.abort());\nif (!res.destroyed && !res.writableEnded) {\n  await writeReadableStreamToWritable(webStream, res);\n}","handlingStrategy":"try-catch","validationCode":"// guard before wiring the adapter\nif (res.writableEnded || res.destroyed) {\n  return; // response is already gone — nothing to write\n}","typeGuard":null,"tryCatchPattern":"try {\n  await writeReadableStreamToWritable(webStream, res);\n} catch (err) {\n  // Client disconnected / stream closed: abort the render, never retry\n  abortController.abort();\n  if (!res.destroyed) res.destroy();\n}","preventionTips":["Abort the request/render when the response emits 'close' so no writes are attempted afterwards","Never call res.end() manually around the streaming adapter","Log disconnect-caused stream errors at debug, not error, level to avoid alert noise"],"tags":["node","streaming","ssr","stream-lifecycle","adapter"],"backgroundTag":"write-after-end","analyzedSha":"917e6fdc0f8beb141e3380e83e27634d7be84e1d","analyzedAt":"2026-08-28T11:01:48.259Z","contentChangedAt":"2026-08-28T11:01:48.259Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}