Skip to content

Clarify that ReadableStream's cancel may be invoked before start is complete #1344

@ardislu

Description

@ardislu

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:

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:

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 and 10.5 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:

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:

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();
    }
  });
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions