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

  1. Treat it as a disconnect signal: catch it, cancel/abort the render source, and let the response die — do not retry
  2. In custom adapters, listen for 'close' on the response and abort the request/render so no further writes are attempted
  3. 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

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


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)