node.js
async.each
callback
asynchronous programming
javascript

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.

javascript
1const async = require("async");
2
3const items = [1, 2, 3];
4
5async.each(
6  items,
7  (item, done) => {
8    setTimeout(() => {
9      console.log("processed", item);
10      done();
11    }, 100);
12  },
13  error => {
14    if (error) {
15      console.error("loop failed:", error);
16      return;
17    }
18
19    console.log("all items finished");
20  }
21);

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).

javascript
1async.each(
2  items,
3  (item, done) => {
4    if (item === 2) {
5      return done(new Error("bad item"));
6    }
7
8    done();
9  },
10  error => {
11    console.log(error ? error.message : "success");
12  }
13);

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.

javascript
1const async = require("async");
2
3async function run() {
4  await async.each([1, 2, 3], async item => {
5    await new Promise(resolve => setTimeout(resolve, 100));
6    console.log("processed", item);
7  });
8
9  console.log("all items finished");
10}
11
12run().catch(error => {
13  console.error("loop failed:", error);
14});

This gives you one clear completion point without nesting a final callback manually.

If you need to avoid too much concurrency, switch to eachLimit:

javascript
1async.eachLimit(
2  [1, 2, 3, 4, 5],
3  2,
4  async item => {
5    await new Promise(resolve => setTimeout(resolve, 100));
6    console.log("processed", item);
7  }
8).then(() => {
9  console.log("limited loop finished");
10});

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.each is done.
  • In promise style, completion is when the returned promise resolves.
  • Every iteratee must signal completion on every code path.
  • Use eachLimit or eachSeries when concurrency needs control.
  • Most "never finishes" bugs come from iteratees that forget to report completion.

Course illustration
Course illustration

All Rights Reserved.