awslabs/llrt · error · TypeError
Illegal constructor
Error message
Illegal constructor
What it means
ReadableStreamDefaultController's JS-exposed constructor deliberately throws a TypeError('Illegal constructor'). Per the WHATWG streams spec, ReadableStreamDefaultController cannot be constructed directly by user code — it is created internally by ReadableStream. The #[qjs(constructor)] binding exists so attempts fail with this spec-mandated message rather than a runtime crash.
Solutions
- Do not construct the controller directly; use `new ReadableStream({ start, pull, cancel })` and access it via the stream's internal controller.
- If you need custom behavior, implement it in the underlying source algorithms rather than subclassing the controller.
- For tests, obtain the controller from a ReadableStream instance instead of new-ing one.
Example fix
// before
const controller = new ReadableStreamDefaultController(); // throws
// after
const stream = new ReadableStream({
start(c) {
c.enqueue('data'); // c is the default controller
}
}); Defensive patterns
Strategy: try-catch
Validate before calling
if (ctor === ReadableStreamDefaultController) throw new TypeError('Illegal constructor: use new ReadableStream() instead'); Try / catch
try {
const c = new ReadableStreamDefaultController();
} catch (e) {
if (e instanceof TypeError && e.message === 'Illegal constructor') {
stream = new ReadableStream({ start(c) { /* controller available here */ } });
} else throw e;
} Prevention
- Never instantiate ReadableStreamDefaultController/ByteLengthQueuingBuffer-style internal classes directly.
- Access the controller through the ReadableStream underlying-source callbacks.
- Run web platform tests to catch spec-incompatible stream usage early.
When it happens
Trigger: Executing `new ReadableStreamDefaultController(...)` in JS — the only way to trigger it; legitimate use is always indirect via `new ReadableStream({ ... })`.
Common situations: Code ported from Node's internal-stream usage or web-platform-test-style code that instantiates controllers directly; misunderstanding that the controller is an internal slot of ReadableStream, not a public class.
AI-assisted analysis of awslabs/llrt@742fc00b82 (2026-09-12).
Data as JSON: /api/errors/bc775a89ea3a75a6.
Report an issue: GitHub.
Appendix: source
Thrown at modules/llrt_stream_web/src/readable/default_controller.rs:624
let promise_primordials = objects.stream.promise_primordials.clone();
let objects_class = objects.into_inner();
Ok((
cancel_algorithm.call(ctx, &promise_primordials, reason)?,
objects_class,
))
}
}
#[methods(rename_all = "camelCase")]
impl<'js> ReadableStreamDefaultController<'js> {
// this is required by web platform tests for unclear reasons
fn constructor() -> Self {
unimplemented!()
}
#[qjs(constructor)]
fn new(ctx: Ctx<'js>) -> Result<Class<'js, Self>> {
Err(Exception::throw_type(&ctx, "Illegal constructor"))
}
// readonly attribute unrestricted double? desiredSize;
#[qjs(get)]
fn desired_size(&self) -> Null<f64> {
let stream = OwnedBorrow::from_class(self.stream.clone());
self.readable_stream_default_controller_get_desired_size(&stream)
}
// undefined close();
fn close(ctx: Ctx<'js>, controller: This<OwnedBorrowMut<'js, Self>>) -> Result<()> {
let objects = ReadableStreamObjects::from_default_controller(controller.0);
// If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, throw a TypeError exception.
if !objects
.controller
.readable_stream_default_controller_can_close_or_enqueue(&objects.stream)View on GitHub (pinned to 742fc00b82)