{"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":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-router-node/stream.ts","lineNumber":105,"sourceCode":"  function onClose() {\n    reject(new Error(\"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 Error(\"Cannot write to a destroyed or ended writable stream\");\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\n    cleanup = function cleanup() {\n      writable.off(\"drain\", onDrain);","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/remix-run/react-router/blob/1fd704a7dabcbe3ae09d7387b460e6acaba30ec1/packages/react-router-node/stream.ts#L87-L123","documentation":"Thrown by monitorWritableError's throwIfClosed() (stream.ts) when writeReadableStreamToWritable attempts to read/write but writable.destroyed or writable.writableEnded is already true. This protects against pushing data into a Node Writable that the consumer has already ended or errored — the underlying stream would emit 'write after end' / 'Error [ERR_STREAM_DESTROYED]'. The monitor also captures prior 'error'/'close' events and rethrows those first.","triggerScenarios":"writeReadableStreamToWritable(stream, writable) is called after the response writable was already .end()'d or .destroy()'d by middleware/upstream code. Or the writable emits 'close' mid-flight, then the loop calls throwIfClosed before the next read/write.","commonSituations":"An Express/Node adapter that calls res.end() prematurely, then the framework tries to pipe a ReadableStream to it. A custom server adapter that destroys the response on a timeout while a stream is being written. Calling writeReadableStreamToWritable twice on the same writable. AbortController triggering res.destroy() during streaming.","solutions":["Ensure the writable passed to writeReadableStreamToWritable has NOT been ended/destroyed before the call (don't double-end responses).","If using an abort signal, stop producing the ReadableStream and let the function unwind instead of destroying the writable externally.","In custom adapters, only call res.end()/res.destroy() AFTER writeReadableStreamToWritable resolves/rejects.","Add a guard around the call site: if (res.writableEnded || res.destroyed) return;"],"exampleFix":"// custom node adapter — before\nexport async function sendResponse(webRes, nodeRes) {\n  // … copy headers/status …\n  nodeRes.end();                       // ends BEFORE the stream is written\n  if (webRes.body) await writeReadableStreamToWritable(webRes.body, nodeRes);\n}\n// after\nexport async function sendResponse(webRes, nodeRes) {\n  // … copy headers/status …\n  if (webRes.body) {\n    await writeReadableStreamToWritable(webRes.body, nodeRes); // ends internally on done\n  } else {\n    nodeRes.end();\n  }\n}","handlingStrategy":"type-guard","validationCode":"import type { Writable } from 'node:stream';\nfunction isWritableReady(w: Writable): boolean {\n  return !w.destroyed && !w.writableEnded;\n}\nif (!isWritableReady(nodeRes)) throw new Error('Response writable is ended/destroyed before stream write');","typeGuard":"function isWritableReady(w: { destroyed: boolean; writableEnded: boolean }): boolean {\n  return !w.destroyed && !w.writableEnded;\n}","tryCatchPattern":"try { await writeReadableStreamToWritable(stream, writable); }\ncatch (e) {\n  if (e instanceof Error && /destroyed or ended writable stream/.test(e.message)) {\n    // the response was closed prematurely; nothing more to write, log and stop\n  }\n  throw e;\n}","preventionTips":["Never call res.end()/res.destroy() before writeReadableStreamToWritable resolves.","Guard adapters: if (res.writableEnded || res.destroyed) return;","Let the helper own the writable lifecycle (it ends on stream completion)."],"tags":["streaming","node","writable","adapter","ssr"],"backgroundTag":null,"analyzedSha":"1fd704a7dabcbe3ae09d7387b460e6acaba30ec1","analyzedAt":"2026-08-12T13:54:57.804Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}