denoland/deno · error · TypeError

ReadableByteStreamController's stream is not in a readable s

Error message

ReadableByteStreamController's stream is not in a readable state

What it means

Thrown by ReadableByteStreamController.close() when the controller's stream state is not "readable" (ext/web/06_streams.js), i.e. the stream is already "closed" or "errored". Once a stream has been closed by a previous close, errored via controller.error(e), or canceled by its reader, its state leaves "readable" and no further close is accepted.

Source

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

  get byobRequest() {
    webidl.assertBranded(this, ReadableByteStreamControllerPrototype);
    return readableByteStreamControllerGetBYOBRequest(this);
  }

  /** @returns {number | null} */
  get desiredSize() {
    webidl.assertBranded(this, ReadableByteStreamControllerPrototype);
    return readableByteStreamControllerGetDesiredSize(this);
  }

  /** @returns {void} */
  close() {
    webidl.assertBranded(this, ReadableByteStreamControllerPrototype);
    if (this[_closeRequested] === true) {
      throw new TypeError("Closed already requested.");
    }
    if (this[_stream][_state] !== "readable") {
      throw new TypeError(
        "ReadableByteStreamController's stream is not in a readable state",
      );
    }
    readableByteStreamControllerClose(this);
  }

  /**
   * @param {ArrayBufferView} chunk
   * @returns {void}
   */
  enqueue(chunk) {
    webidl.assertBranded(this, ReadableByteStreamControllerPrototype);
    const prefix =
      "Failed to execute 'enqueue' on 'ReadableByteStreamController'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    const arg1 = "Argument 1";
    chunk = webidl.converters.ArrayBufferView(chunk, prefix, arg1);
    let buffer, byteLength;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pick one terminal operation per stream: either close() on success or error(e) on failure, never both.
  2. Before closing, check controller.desiredSize: null means errored, 0 strongly suggests already-closed; treat both as 'do not close'.
  3. Propagate the reader's cancel signal to your producer (implement cancel() in the underlying source) so cleanup knows not to close.
  4. Wrap close() in try/catch TypeError and ignore when the stream already ended.

Example fix

// before
try {
  controller.enqueue(chunk);
  controller.close();
} catch (e) {
  controller.error(e);
  controller.close(); // throws: stream is errored
}

// after
try {
  controller.enqueue(chunk);
  controller.close();
} catch (e) {
  controller.error(e); // terminal; do not also close
}
Defensive patterns

Strategy: try-catch

Validate before calling

// desiredSize: null => errored, 0 => likely closed (verify with your
// own state). Only close when the stream is provably still readable.
const ds = controller.desiredSize;
if (ds !== null && ds > 0 && !this.done) {
  controller.close();
}

Try / catch

try {
  controller.close();
} catch (err) {
  if (err instanceof TypeError && /not in a readable state/.test(err.message)) {
    // stream already closed/errored/canceled — nothing to do
  } else throw err;
}

Prevention

When it happens

Trigger: Calling close() after controller.error(e) in an error path; calling close() after the consumer called stream.cancel(); closing after the stream already reached "closed" state (e.g. close after close where the first close completed immediately).

Common situations: Error handling that calls error(e) and then generic cleanup calls close(); server code closing a stream after a client disconnect already canceled it; retry wrappers that close on exit while the stream ended earlier.

Related errors


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