[whatwg/streams] Clarify that `ReadableStream`'s `cancel` may be invoked before `start` is complete (Issue #1344)

ardislu created an issue (whatwg/streams#1344)

### What is the issue with the Streams Standard?

If `cancel` is called immediately after a new `ReadableStream` is instantiated, the logic in `start` may not be completed before `cancel` executes. For example:

```javascript
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const r = new ReadableStream({
  async start() {
    await sleep(1000);
    console.log('start completed');
  },
  cancel() {
    console.log('cancel completed');
  }
});
await r.cancel();
// cancel completed
// start completed
```

As I understand it, this behavior is by design. However, it may be confusing because `close`  and `abort` on `WritableStream` *do* wait for `start` to complete:

```javascript
const w = new WritableStream({
  async start() {
    await sleep(1000);
    console.log('start completed');
  },
  close() {
    console.log('close completed');
  }
});
await w.close();
// start completed
// close completed

const w2 = new WritableStream({
  async start() {
    await sleep(1000);
    console.log('start completed');
  },
  abort() {
    console.log('abort completed');
  }
});
await w2.abort();
// start completed
// abort completed
```

**In particular**, examples [10.4](https://streams.spec.whatwg.org/#example-rs-pull) and [10.5](https://streams.spec.whatwg.org/#example-rbs-pull) on the standard imply that `cancel` waits for `start` to finish before executing because there is no check that `fs.open` has completed before trying to call `fileHandle.close`:

```javascript
const fs = require("fs").promises;

function makeReadableFileStream(filename) {
  let fileHandle;

  return new ReadableStream({
    async start() {
      fileHandle = await fs.open(filename, "r");
    },

    // ...

    cancel() {
      return fileHandle.close(); // Dangerous: fileHandle may still be undefined
    }
  });
}
```

It would be useful if examples 10.4 and 10.5 made an explicit note about this behavior. The example code can also be updated to block `cancel` until `start` is completed. For example:

```diff
const fs = require("fs").promises;

function makeReadableFileStream(filename) {
  let fileHandle;

+ const { promise, resolve } = Promise.withResolvers();

  return new ReadableStream({
    async start() {
      fileHandle = await fs.open(filename, "r");
+     resolve();
    },

    // ...

-   cancel() {
+   async cancel() {
+     await promise;
      return fileHandle.close();
    }
  });
}
```

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/streams/issues/1344
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/streams/issues/1344@github.com>

Received on Monday, 28 April 2025 07:22:47 UTC