Skip to content

Proposal: ReadableStream.withSafeResolvers() #1359

@juner

Description

@juner

What problem are you trying to solve?

There is no ergonomic, standardized way to externally control a ReadableStream (i.e., enqueue, close, or error) with a safe interface.

Today:

  • The only way is to keep a reference to ReadableStreamDefaultController.
  • Calling controller.enqueue(), controller.close(), or controller.error() after the stream is closed/errored/canceled may throw.
  • Developers sometimes implement ad-hoc helpers ("withResolvers"-style wrappers) to build "pushable streams", but this pattern is not standardized.

This makes event-based or imperative stream production awkward and error-prone.

What solutions exist today?

  1. Manually handling ReadableStreamDefaultController

    • Requires writing boilerplate.
    • Unsafe: calling methods after close/error/cancel throws.
    • Requires custom guarding logic.
  2. Ad-hoc helper utilities

    • Non-standard.
    • Inconsistent behavior (especially around cancellation and error propagation).
  3. Async generator wrappers

    • Cannot expose a truly push-based API.
    • Cannot support backpressure-compatible push-style enqueuing.

How would you solve it?

Introduce a resolver-style API, similar to Promise.withResolvers():

interface ReadableStream {
  static withSafeResolvers<T = unknown>(): {
    stream: ReadableStream<T>;
    enqueue(chunk: T): void;
    close(): void;
    error(reason: unknown): void;
  };
}

Key behavior

  • enqueue(chunk) — enqueues a chunk; ignored if closed/errored/canceled.
  • close() — closes the stream safely; ignored after finalization.
  • error(reason) — errors the stream; ignored after finalization.
  • All operations after the stream is finalized (closed, errored, or canceled) are silently ignored.

This provides an ergonomic, safe way to create externally controlled pushable streams.

Anything else?

A reference implementation exists:

This proposal maintains compatibility with all existing stream semantics (including backpressure). The API surface is minimal and follows existing web-standard precedents such as Promise.withResolvers().

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions