denoland/deno · error · TypeError

Cannot enqueue chunk when underlying stream is not readable

Error message

Cannot enqueue chunk when underlying stream is not readable

What it means

Thrown by ReadableByteStreamController.enqueue(chunk) when the stream's state is not "readable" (ext/web/06_streams.js) — the stream was canceled by its reader, errored via controller.error(), or already closed. Chunks cannot be delivered to a non-readable stream, so enqueue is rejected.

Source

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

        prefix,
        arg1,
      );
    }
    if (getArrayBufferByteLength(buffer) === 0) {
      throw webidl.makeException(
        TypeError,
        "Buffer length must be non-zero",
        prefix,
        arg1,
      );
    }
    if (this[_closeRequested] === true) {
      throw new TypeError(
        "Cannot enqueue chunk after a close has been requested",
      );
    }
    if (this[_stream][_state] !== "readable") {
      throw new TypeError(
        "Cannot enqueue chunk when underlying stream is not readable",
      );
    }
    return readableByteStreamControllerEnqueue(this, chunk);
  }

  /**
   * @param {any=} e
   * @returns {void}
   */
  error(e = undefined) {
    webidl.assertBranded(this, ReadableByteStreamControllerPrototype);
    if (e !== undefined) {
      e = webidl.converters.any(e);
    }
    readableByteStreamControllerError(this, e);
  }

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Implement cancel(reason) in the underlying source and stop enqueueing once it fires.
  2. Before enqueueing, check controller.desiredSize — null means errored; treat 0-with-no-progress plus a done flag as ended; on any terminal signal stop producing.
  3. Route all enqueue calls through one helper that first checks your producer's done/canceled state.
  4. Clear timers/callbacks in the cancel path so no enqueue can race cancellation.

Example fix

// before
new ReadableStream({
  start(c) { setInterval(() => c.enqueue(poll()), 10); }, // keeps firing after cancel
});

// after
new ReadableStream({
  start(c) {
    this.t = setInterval(() => { if (!this.done) c.enqueue(poll()); }, 10);
  },
  cancel() { this.done = true; clearInterval(this.t); },
});
Defensive patterns

Strategy: validation

Validate before calling

// Stop when the consumer canceled: desiredSize is null once errored,
// and cancellation closes the stream. Combine with your own done flag.
function safeEnqueue(controller, chunk, done) {
  if (done || controller.desiredSize === null) {
    stopProducing(); // cancel timers, unsubscribe events
    return;
  }
  controller.enqueue(chunk);
}

Try / catch

try {
  controller.enqueue(chunk);
} catch (err) {
  if (err instanceof TypeError && /not readable/.test(err.message)) stopProducing();
  else throw err;
}

Prevention

When it happens

Trigger: The consumer calls stream.cancel() (e.g. aborted fetch, client disconnect) while the producer keeps enqueueing; enqueueing after controller.error(e) already errored the stream; enqueueing after the stream closed.

Common situations: Long-running producers that ignore cancellation (no cancel() implementation in the underlying source); server handlers whose client aborted mid-stream; error paths that error the stream but leave timers/callbacks pushing data.

Related errors


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