node.js async.each callback, how do I know when it's done?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
With async.each, you know the whole operation is finished when the final callback runs, or when the returned promise resolves if you are using the promise-capable form. Most bugs happen not at the end, but inside the iteratee when one item never signals completion.
Use the Final Callback as the Completion Signal
In classic callback style, async.each takes three pieces: the collection, the per-item iteratee, and the final callback.
That final callback is the answer to "how do I know when it is done?" If it runs without an error, all iteratees completed successfully.
Make Sure Every Iteration Signals Completion
async.each does not infer completion automatically. Every branch of the iteratee must call done() or done(error).
If one branch returns early without calling done, the whole loop can appear to hang forever because one item is still considered in progress.
Use Promise Style in Modern Code
Newer code is often easier to read if you keep the iteratee promise-based and await the whole each call.
This gives you one clear completion point without nesting a final callback manually.
If you need to avoid too much concurrency, switch to eachLimit:
If strict ordering matters rather than just completion, use eachSeries instead of each. That runs one item at a time and makes the completion point easier to reason about for workflows with sequencing requirements.
Common Pitfalls
The biggest mistake is forgetting to call the per-item callback in one code path. That prevents the final callback from ever running.
Another common issue is mixing callback-style iteratees with async functions in the same async.each call. Pick one completion model and stick to it, or the control flow becomes ambiguous.
People also assume async.each is serial. It is not. Items can run concurrently, which means shared mutable state inside the iteratee needs care. If order matters, use eachSeries instead.
Finally, do not use the final callback to infer business success unless you are handling iteratee errors correctly. A swallowed error in the iteratee can still give you misleading completion behavior.
If you are debugging a stuck loop, add logging right before every done() call. That usually shows exactly which branch failed to signal completion and kept the final callback from firing.
Summary
- In callback style, the final callback tells you when
async.eachis done. - In promise style, completion is when the returned promise resolves.
- Every iteratee must signal completion on every code path.
- Use
eachLimitoreachSerieswhen concurrency needs control. - Most "never finishes" bugs come from iteratees that forget to report completion.

