denoland/deno · error · TypeError

Readable stream is unavailable

Error message

Readable stream is unavailable

What it means

TransformStreamDefaultController.enqueue() was called while the TransformStream's readable side can no longer accept chunks — it is closed or errored, so readableStreamDefaultControllerCanCloseOrEnqueue is false. Typical causes: the consumer cancelled the readable side, a previous transform already called controller.close() or controller.error(), or enqueue runs after the stream finished.

Source

Thrown at ext/web/06_streams.js:4848

  controller[_transformAlgorithm] = undefined;
  controller[_flushAlgorithm] = undefined;
  controller[_cancelAlgorithm] = undefined;
}

/**
 * @template O
 * @param {TransformStreamDefaultController<O>} controller
 * @param {O} chunk
 */
function transformStreamDefaultControllerEnqueue(controller, chunk) {
  const stream = controller[_stream];
  const readableController = stream[_readable][_controller];
  if (
    readableStreamDefaultControllerCanCloseOrEnqueue(
      /** @type {ReadableStreamDefaultController<O>} */ readableController,
    ) === false
  ) {
    throw new TypeError("Readable stream is unavailable");
  }
  try {
    readableStreamDefaultControllerEnqueue(
      /** @type {ReadableStreamDefaultController<O>} */ readableController,
      chunk,
    );
  } catch (e) {
    transformStreamErrorWritableAndUnblockWrite(stream, e);
    throw stream[_readable][_storedError];
  }
  const backpressure = readableStreamDefaultcontrollerHasBackpressure(
    /** @type {ReadableStreamDefaultController<O>} */ readableController,
  );
  if (backpressure !== stream[_backpressure]) {
    assert(backpressure === true);
    transformStreamSetBackpressure(stream, true);
  }
}

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Check controller.desiredSize — it is null exactly when the readable side is closed or errored; skip the enqueue then.
  2. Do not enqueue after close() or error(); make flush() a no-op once the readable side is gone.
  3. Treat downstream cancellation as a stop signal: abort outstanding work instead of continuing to transform.

Example fix

// before
transform(chunk, controller) {
  controller.close();
  controller.enqueue(chunk); // readable closed -> TypeError
}

// after
transform(chunk, controller) {
  if (controller.desiredSize === null) return; // readable gone
  controller.enqueue(chunk);
}
Defensive patterns

Strategy: validation

Validate before calling

// inside transform()/flush()
if (controller.desiredSize === null) {
  return; // readable side closed or errored — do not enqueue
}
controller.enqueue(out);

Try / catch

try {
  controller.enqueue(out);
} catch (e) {
  if (e instanceof TypeError && e.message === 'Readable stream is unavailable') {
    return; // consumer is gone: stop transforming, let cancellation propagate
  }
  throw e;
}

Prevention

When it happens

Trigger: controller.enqueue(chunk) after controller.close() in the same transformer; enqueueing after controller.error(...) or after an earlier enqueue threw; async work started in transform() resolving after the consumer cancelled the readable side.

Common situations: Transformers emitting a final chunk from flush() after closing early; fire-and-forget promises inside transform(); multi-stage pipelines where a downstream stage errored or the client disconnected.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/06f2750df8f4cbdd. Report an issue: GitHub.