Why node.js async module stops after the first step using async.eachLimitarray, limit, function, callback?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When async.eachLimit appears to stop after the first item, the problem is almost never the library itself. In nearly every case, the iterator callback is not being called correctly, is being called more than once, or an error is being passed into it and causing the whole loop to end early.
That is the central rule of the async callback style: each item must complete exactly once, and the final callback runs early if any iterator reports an error.
How eachLimit Actually Works
eachLimit(array, limit, iterator, done) runs up to limit iterator calls at the same time. Each iterator receives one item and a callback. The library assumes the item is still in progress until that callback is invoked.
A minimal correct example looks like this:
If callback() is never reached for an item, the loop appears to hang. If callback(err) is called, the loop stops and the final callback receives that error.
The Most Common Failure: Missing Callback
The usual bug is that one branch of the iterator forgets to call the callback:
This looks harmless, but the first item never signals completion. From the library's point of view, the operation is still running forever.
The fix is to make sure every code path reaches the callback:
The Second Common Failure: Error Ends The Loop
eachLimit is fail-fast. If one iterator passes an error, the final callback fires and the loop stops scheduling new work.
That is correct behavior, not a random stop. If you want to keep processing even when some items fail, handle the error inside the iterator and call callback() without passing the error upstream.
Do Not Mix Callback Style And Promise Style Carelessly
Another trap appears when code mixes callback conventions with async or await without understanding which style the function expects. In the classic callback form, you should finish by calling the provided callback. In promise-aware usage, you should return a promise and not manually use the callback at the same time.
If you mix both, you can end up with double completion or with code paths that never complete at all.
Debug The Iterator, Not The Loop
When eachLimit "stops after the first step," add logging around the iterator entry and callback exit. You usually discover that:
- the iterator never calls
callback - an exception prevents the callback from running
- an early
returnskips the callback - an error is passed intentionally and terminates the whole flow
That is why the debugging target is the iterator implementation, not the concurrency limit itself.
Common Pitfalls
One common mistake is forgetting to call the iterator callback on one branch of logic. Another is calling it twice, which creates erratic flow-control bugs. Developers also often misread fail-fast error handling as "the loop stopped randomly," when the loop is actually honoring callback(err). Finally, mixing callbacks with promises in the same iterator without a clear ownership model is a reliable way to produce hangs or double-finish behavior.
Summary
- '
async.eachLimitcontinues only when each iterator calls its callback exactly once.' - If the iterator never calls the callback, the loop appears stuck.
- If the iterator passes an error,
eachLimitstops early by design. - Debug the iterator code paths, not just the loop declaration.
- Pick one async style per iterator: callback-based or promise-based, not both carelessly mixed.

