Repository navigation
Race condition when ending a stream #40471
Description
Activity
- addedstreamIssues and PRs related to Node.js streams.Issues and PRs related to Node.js streams.
on Oct 15, 2021 From my studying of the node code, I'm starting to think that the behavior of "waiting forever" is the correct one -- and therefore that v14.x and v16.x fixed issues.
Let me explain (because I find that the documentation isn't always good enough at this level of details). This is my understanding and may contain also imprecisions.
src.pipe(dest)
=> whensrcemitsend, it will calldest.end()automatically (unless we pass the optionend: false, in this case it will onlyunpipe).
=> whendestemitsfinishorclose, it will callsrc.unpipe(dest)automatically... but notsrc.end()(which makes sense).
=> no handling of errorspipeline(src, dest, callback)
=> does a lot of magic with things like async generators etc, but for regular node streams this callssrc.pipe(dest)
=> the callback is called only when all streams are ended/finished (using the same function thanstream.finished).
=> because it usedstream.finished, it also reacts to errors, and in that case it willdestroyall streams in the pipeline.Therefore, what happens in the case above:
this.end()is called a very short term after the firstwrite=>unpipeis called- The source
PassThroughis waiting forever for its data to be consumed, until then it doesn't emit theendevent, and therefore the pipeline never ends either.
I now think the faulty behaviors in v12.x and v14.x were due to a race or several races around the use of
nextTick.:- it would end "properly" (my initial expectation) if the call to
endwas handled a bit later. - The error
[ERR_STREAM_WRITE_AFTER_END]: write after endwould happen ifunpipewasn't called in a timely manner.
I couldn't pinpoint which different code produces these different behaviors yet.
This is more puzzling. I tried simplifying my testcase, removing all the piping. This is the
runfunction with the sameWritableimplementation.finishedis a promisifiedStream.finished.console.log("START TEST"); const fixture = "WRITE SOMETHING"; const checker = new MyStream(); console.log("WRITE 1"); checker.write(fixture.slice(0, 3)); await nextTick(); console.log("WRITE 2"); checker.end(fixture.slice(3)); console.log("WAIT FOR PIPELINE END"); await finished(checker); console.log("FINISHED");
Now, v12 gives me the error I expect:
Error [ERR_STREAM_WRITE_AFTER_END]: write after end, but v14 and v16 runs it to the end... Could that be a real bug?
I get the error if I register an "error" event handler, which is a good thing.I fixed my issue by removing the call to
endinside the stream itself, instead emitting a custom event, and handling it outside of the stream.github-actions commented
on Jun 26, 2026 on Jun 26, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jun 26, 2026 github-actions commented
on Jul 27, 2026 on Jul 27, 2026 – with GitHub ActionsContributorMore actionsThis issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.
Version
v14.17.5 and v16.11.1
Platform
Linux torri 5.13.14 #29 SMP Mon Sep 6 12:28:17 CEST 2021 x86_64 GNU/Linux
Subsystem
stream
What steps will reproduce the bug?
Clone https://github.com/julienw/streams-problems
Run the script
node-streams.jswith node v14 or node v16.Here is the content of this script:
(This needs to be run with
type:"module"so it's probably easier to clone the repository)In v12, we get this output:
But in v14 and v16 we get this:
And this never ends.
As you see, we're calling
this.end()in_write()after a nextTick. The original idea was that this stream's purpose was finished (checking something) and callingendwould unpipe, and we wouldn't get more writes. The goal is to stop doing more work.How often does it reproduce? Is there a required condition?
Always
What is the expected behavior?
I think we should get one of these results:
end([ERR_STREAM_WRITE_AFTER_END]: write after end).What do you see instead?
Here it looks like that the pipeline isn't unpiped AND we don't get an error. So the written data isn't consumed by the stream that's been ended, and we're waiting forever.
Additional information
Replacing
process.nextTick(() => this.end());with something likeawait nextTick(); this.end();gets an error (behavior 2 above),which is different than node v12but at least we're not waiting forever and we get some clue. Update Oct 18: we get the same error in v12 and v14, but in v16 we get the behavior of waiting forever.Any insight will be very much appreciated. Especially can you point to what changed in v14 in this topic compared to v12 (I do know that a lot of the streams code changed in-between)? Also it seems to be than waiting forever isn't the right behavior.
Thanks!