Node.js Streams onend completing before asynchronous onreadable completed
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
This issue appears when a readable stream emits end, but the asynchronous work started inside readable callbacks is still running. That is expected behavior: Node streams know when no more chunks are available, but they do not automatically wait for your custom promises. To make completion reliable, you need to connect stream completion and async task completion explicitly.
Why end Does Not Wait for Your Async Work
The readable event only tells you that data can be pulled from the internal buffer. If your handler starts an asynchronous function, the stream does not track it. The end event fires once the source has no more data, not when your database writes, HTTP calls, or timers finish.
This simplified example shows the race:
stream ended can appear before the last processed line because the stream only knows that reading is complete. It has no knowledge of the await inside your handler.
Prefer Async Iteration for Sequential Processing
If you want to read each chunk and wait for async work before moving on, for await...of is the cleanest pattern. It ties chunk consumption to your async function naturally.
This approach is easier to reason about than mixing event listeners and await. It also makes error handling straightforward because thrown exceptions become rejected promises that you can catch in one place.
Track In-Flight Work If You Must Use Events
Sometimes you need to keep an event-based design. In that case, maintain your own list of pending promises and wait for them after end.
This works, but you are now responsible for managing memory, failures, and backpressure. If readable can produce thousands of chunks quickly, pending may grow too large.
Use pipeline and Transforms for Stream-Shaped Work
When the job is truly stream processing rather than chunk collection, a Transform stream or pipeline is often the better model. It keeps flow control inside the stream system.
pipeline gives you one promise that settles when the whole chain finishes or fails. That is usually the completion signal people expect from end.
Choose the Pattern Based on Ownership
If you own the whole consumer, prefer async iteration. If you are extending an event-based system, track pending work explicitly. If the problem is stream processing end to end, redesign around pipeline. The bug usually comes from mixing models: event handlers are being used as if they were promise-aware completion hooks.
Common Pitfalls
- Assuming
endmeans all asynchronous work launched fromreadablehas completed. - Using an
asyncevent listener and expecting Node to wait for the returned promise automatically. - Accumulating unbounded pending promises when
readableemits faster than downstream work completes. - Ignoring
errorhandling while focusing only onreadableandend. - Mixing event-driven reading with stream transforms when a
pipelinedesign would be simpler and safer.
Summary
- '
endonly signals that no more stream data will arrive.' - Async work started in
readablehandlers must be tracked separately. - '
for await...ofis the clearest pattern for sequential async chunk processing.' - '
Promise.allover tracked tasks can work in event-based code, but requires care.' - '
pipelineis often the best fit when the whole workflow is naturally stream-shaped.'

