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

  1. Do not construct the controller directly; use `new ReadableStream({ start, pull, cancel })` and access it via the stream's internal controller.
  2. If you need custom behavior, implement it in the underlying source algorithms rather than subclassing the controller.
  3. 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

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)