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
- Close exactly once: guard with a `closed` boolean shared by all paths that can close.
- Only call close() from the single code path that detects source EOF.
- 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
- Use one guarded close helper for all call sites.
- Let only the EOF branch of your producer close the controller.
- In tests of stream glue code, assert close is called exactly once.
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
- Closed already requested.
- The BYOB request's buffer has been detached and so cannot be
- ReadableByteStreamController's stream is not in a readable s
- Cannot enqueue chunk after a close has been requested
- Cannot enqueue chunk when underlying stream is not readable
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/bf25087cd62e86d2.
Report an issue: GitHub.