Skip to content

If abort() called before reader attached, all writes fail; if abort() called while promise argument of a write() call has not resolved, that write will fail when it resolves #50

@MattiasMartens

Description

@MattiasMartens

Wanted to flag this “gotcha” that I've encountered a few times.

Case 1 - calling abort() before calling something that attaches a reader to the stream:

(async () => {
	const s = new Stream();
	s.aborted().catch(() => ({})); // Swallow abort exception
	s.write(1); // This won't be pumped until toArray() is called below
	s.abort(); // The write above is now doomed!
	s.end();
	console.log(await s.toArray()); // Promise of s.write(1) will now reject as toArray() attempts to pull from aborted source
})();

Case 2 - calling abort() while the promise-like argument of write() has yet to resolve:

(async () => {
	const s = new Stream();
	s.aborted().catch(() => ({})); // Swallow abort exception
        const p = s.toArray(); // Attach reader before abort(), to avert case 1
	s.write(delay(2).then(() => 1)); // Wait 2 ms before resolving 1
	s.abort(); // No more writes past this point; but the promise generated by `delay(2).then(() => 1)` will try to write in the future, and fail
	s.end();
	console.log(await p);
})();

The first, to me, seems like a bug. write() was clearly called before abort().

The second one is less clear... According to the specification for abort(), this write should fail because it can no longer be processed downstream.

But it creates a problem for the source, which must not only detect abort() and stop writing, but also handle any unresolved promises it already passed downstream (presumably by catching the "aborted" errors from the corresponding write calls).

I don't really have a fix in mind for case 2. I considered simply ignoring promise resolutions in this case, but that violates the guarantee that writes must either succeed and pass the value to the reader or fail. I also considered modifying abort() to wait for resolution of promises sent to write(), but that would violate the expectation that abort() is supposed to be synchronous.

Maybe the best thing to do is to document the gotcha. I would also suggest that instead of post-abort writes throwing the abort error, they should throw a "WriteAfterAbortError" that wraps that error. This would make it easier to distinguish from the case where the source's aborted() rejection was not handled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions