denoland/deno · error · TypeError

The stream controller cannot close or enqueue

Error message

The stream controller cannot close or enqueue

What it means

Thrown by ReadableStreamDefaultController.close() when readableStreamDefaultControllerCanCloseOrEnqueue(this) returns false (ext/web/06_streams.js): either close was already requested, the stream was canceled, or it was errored. The default (value-mode) controller only permits one terminal close while the stream is still readable.

Source

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

  constructor(brand = undefined) {
    if (brand !== _brand) {
      webidl.illegalConstructor();
    }
    this[_brand] = _brand;
  }

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

  /** @returns {void} */
  close() {
    webidl.assertBranded(this, ReadableStreamDefaultControllerPrototype);
    if (readableStreamDefaultControllerCanCloseOrEnqueue(this) === false) {
      throw new TypeError("The stream controller cannot close or enqueue");
    }
    readableStreamDefaultControllerClose(this);
  }

  /**
   * @param {R} chunk
   * @returns {void}
   */
  enqueue(chunk = undefined) {
    webidl.assertBranded(this, ReadableStreamDefaultControllerPrototype);
    if (chunk !== undefined) {
      chunk = webidl.converters.any(chunk);
    }
    if (readableStreamDefaultControllerCanCloseOrEnqueue(this) === false) {
      throw new TypeError("The stream controller cannot close or enqueue");
    }
    readableStreamDefaultControllerEnqueue(this, chunk);
  }

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Close exactly once: guard with a `closed` boolean shared by all paths that can close.
  2. Only call close() from the single code path that detects source EOF.
  3. If state is uncertain, wrap close() in try/catch TypeError and treat 'cannot close or enqueue' as already-ended.

Example fix

// before
function push(controller, chunk, end) {
  if (chunk) controller.enqueue(chunk);
  if (end) controller.close();
}
// ...two calls with end=true throw on the second

// after
let closed = false;
function push(controller, chunk, end) {
  if (closed) return;
  if (chunk) controller.enqueue(chunk);
  if (end) { closed = true; controller.close(); }
}
Defensive patterns

Strategy: validation

Validate before calling

// desiredSize is null when errored and 0 when closed — combined with
// an own close flag this is a reliable pre-check for close().
let closed = false;
function closeIfOpen(controller) {
  if (closed || controller.desiredSize === null) return;
  closed = true;
  controller.close();
}

Try / catch

try {
  controller.close();
} catch (err) {
  if (err instanceof TypeError && err.message === 'The stream controller cannot close or enqueue') return;
  throw err;
}

Prevention

When it happens

Trigger: Calling close() twice; calling close() after enqueue-then-close in start(); calling close() after error(e) or after the reader canceled the stream; closing in a finally that also runs after a normal close.

Common situations: Transform/source implementations with cleanup code that closes unconditionally; double close across async boundaries (e.g. in both pull() completion and an end event); test code that closes stub controllers twice.

Related errors


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