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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions