remix-run/react-router · warning · WritableClosedError
Cannot write to a destroyed or ended writable stream
Error message
Cannot write to a destroyed or ended writable stream
What it means
@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.
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
Example fix
// before — custom Node adapter
res.end(); // ended early (client gone or manual flush)
await writeReadableStreamToWritable(webStream, res); // throws
// after
res.on("close", () => abortController.abort());
if (!res.destroyed && !res.writableEnded) {
await writeReadableStreamToWritable(webStream, res);
} Defensive patterns
Strategy: try-catch
Validate before calling
// guard before wiring the adapter
if (res.writableEnded || res.destroyed) {
return; // response is already gone — nothing to write
} Try / catch
try {
await writeReadableStreamToWritable(webStream, res);
} catch (err) {
// Client disconnected / stream closed: abort the render, never retry
abortController.abort();
if (!res.destroyed) res.destroy();
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- A meta object uses an invalid tagName
- Failed to clone server response
- Invalid redirect location
- () call aborted without an `AbortSignal.reason
- Missing body in server response
AI-assisted analysis of remix-run/react-router@917e6fdc0f (2026-08-28).
Data as JSON: /api/errors/567d62d157b389eb.
Report an issue: GitHub.
Appendix: source
Thrown at packages/react-router-node/stream.ts:135
function onClose() {
reject(new WritableClosedError("Writable closed before stream finished"));
}
writable.once("error", onError);
writable.once("close", onClose);
return {
cleanup,
race<T>(promise: Promise<T>) {
return Promise.race([promise, writableErrorPromise]);
},
throwIfClosed() {
if (writableError) {
throw writableError;
}
if (writable.destroyed || writable.writableEnded) {
throw new WritableClosedError(
"Cannot write to a destroyed or ended writable stream",
);
}
},
};
}
function waitForDrain(
writable: Writable,
writableError: WritableErrorMonitor,
): Promise<void> {
let cleanup = () => {};
let drainPromise = new Promise<void>((resolve) => {
function onDrain() {
cleanup();
resolve();
}
View on GitHub (pinned to 917e6fdc0f)