For years I've been hoarding a secret that's gotten me millions of downloads on npm. Today the secret comes out.

arr.forEachAsync(doStuff).then(doWhenDone);

I'm the author of the much famed forEachAsync.

It's only about 30 lines of implementation, and a little bit of wrapper for support between the browser and node, including older versions. It supports Promises, handles exceptions, and even has a nifty break;-like feature.

It does what it needs to do. It is complete. And, according, to the npm stats it gets over 1 million downloads per month!

And I'm here to tell you that you shouldn't use it, but just copy and paste this instead:

function forEachAsync(arr, fn) {
  function next() {
    var el = arr.shift();
    if (el) {
      return Promise.resolve(fn(el)).then(next);
    }
  }
  return Promise.resolve().then(next);
}

Let's talk about that...

First up:

  • This is sequential, not concurrent (which is what we want for for loops)
  • All exceptions will be caught in .catch(), no need to try { ... } catch(.) { ... }.
  • Works with both synchronous AND asynchronous functions. Talk about a double-whamie! Noice.

Having the promise kick things off rather than calling next() directly is what guarantees that any exceptions will be caught and handled by .catch():

return Promise.resolve().then(next);

Wrapping the work-doing function in a Promise makes it so that we can use sync and async functions with our forEach, which can be convenient for consistency. But it could also suppress an error message if you're accidentally not returning a promise in a function that's intended to be async, so... pick your poison:

// works with both sync and async functions
return Promise.resolve(fn(el)).then(next);

or

// throws an error (no `.then` on `undefined`)
// when the function is missing a Promise return
return fn(el).then(next);

One thing you might not be super familiar with is .shift():

var el = arr.shift();

Often, with arrays, we use .pop() to get an element off. However, if preserving order is important, it makes more since to use .shift(). That said, sometimes using a .reverse() is more readable.

The important take-away is that in a loop we usually do care about order, so we want to handle each item, one at a time, in the same order it was handed to us.

And that's that.

Breaking Promises

If we wanted to give this some break-like functionality (similar to Array.some), we could do this:

function someAsync(arr, fn) {
  function next() {
    var el = arr.shift();
    if (el) {
      return Promise.resolve(fn(el)).then(function (some) {
        if (!some) { next(); }
      });
    }
  }
  return Promise.resolve().then(next);
}

Following that pattern you could even rewrite the whole suite of Array methods in an async way, which I did, long ago (but nobody really uses it).

No Promises at All

Let's say you don't care much Promises though (in which case you're crazy, btw), but here's what that might look like anyway:

function forEachAsync(arr, fn, cb) {
  function next() {
    var el = arr.shift();
    if (!el) { cb(); return; }
    try {
      fn(el, next);
    } catch(e) {
      cb(er);
    }
  }
  return next();
}

Wait a minute... that wasn't 7 lines!

That was 8, no, wait... 9 lines!

Okay, okay. You caught me. I lied. Are you happy now?

Look, this is just how it is when you're famous. Basketball players get to add a few inch to their height, npm superstars get to subtract a few LoC. It's just how it is. Okay?

(then there's off-by-one errors and all that, so who could blame me?)

In all seriousness though, I just really wanted it to be 7 lines - It's easily done too, but I thought that the couple of methods I came up with looked just a little too clever and not quite readable enough.

Of course, we can did it down to just 6 lines if we just remove some newlines and nix one of the vars:

(but we'll keep some brackets for readability)

function forEachAsync(arr, fn) {
  function next() {
    if (arr.length) { return Promise.resolve(fn(arr.shift())).then(next); }
  }
  return Promise.resolve().then(next);
}

Pfff! But what's the fun in that?

Here's one I'm proud of though:

(getting exactly 7 semi-readable lines is actually quite hard)

function forEachAsync(arr, fn) {
  return Promise.resolve().then(function next() {
    if (arr.length) {
      return Promise.resolve(fn(arr.shift())).then(next);
    }
  });
}

Yeah, mom would be proud of that.

(clever use of a privately named anonymous function)

Bam! 7 lines! Are you happy now?

Bonus: bare metal, no chaser

(because, y'know, 10 lines of VanillaJS isn't bare metal enough)

To be honest, I don't even use a forEachAsync function myself anymore. I literally rewrite these same lines whenever I need them. I've got them on brain-macro. And, when I write this for myself, I don't even abstract it - I just go full next():

function next() {
  var el = arr.shift();
  if (!el) { return null; }
  return fn(el).then(next);
}
return Promise.resolve(next());

When I'm writing the code myself I don't need to wrap fn(el) with another Promise because I'm writing the code myself and I know that it's going to return as it should (otherwise it's a bug in my code I need to fix).


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )



Published

2019-4-5



Buy me a coffeeBuy me a coffee


  73% of Goal Reached


Want more like this?