Async Loop Flavor Adventure

Let’s go on a journey through JavaScript Async Flavor Country™

Ah, to loop asynchronously in the year 2018. What a grand adventure! What a time to be alive!

In this day and age, some might find themselves in a state of choice paralysis, overwhelmed by the many possible ways to travel through an array in an asynchronous way. Fret not! This guide is here to comfort the worried mind and soothe the aching heart.

We will be exploring three avenues for approaching the problem of constructing an asynchronous loop:

  1. 📞 Callbacks
  2. 🙏 Promises
  3. ✋ Async/Await

Let’s start with a very contrived example

Let us say we have some asynchronous task like so:

function task (num) {
  setTimeout(function delay () {
    console.log(num)
  }, 1000)
}

And we have a list of items that need to be fed to this task:

const list = Array.from(Array(3).keys())
// => [0,1,2]

The most straightforward solution would be to simply iterate through the list, passing each item to the task function.

list.forEach(task)

However! In an asynchronous program, we will most likely want to do something after all asynchronous operations have completed.

list.forEach(task)
console.log('Done!')
// => Done!
// => 0
// => 1
// => 2

The above is no good. We want to log that we’re done after we’re done.

This is the challenge we’ll be exploring.

Solutions

1. 📞 Callbacks

By modifying the task function to accept a callback, we can call a function once we know the task has been completed.

const list = Array.from(Array(3).keys())

function task (num, callback) {
  setTimeout(function delay () {
    console.log(num)
    callback()
  }, 1000)
}

function queue (list) {
  list.forEach(function (currentValue, index, arr) {
    task(currentValue, function () {
      if (index === arr.length - 1) console.log('Done!')
    })
  })
}

queue(list)
// => 0
// => 1
// => 2
// => Done!

In this case we’re using the arguments provided by the Array.forEach() method to determine if we’ve reached the end of the list within the context of the callback.

The above isn’t too hard to read and works fine in all modern browsers. All it requires is support for ES5.

2. 🙏 Promises

But what about promises? Okay, let’s do promises.

By modifying the task function to return a Promise, we are afforded a somewhat more elegant syntax for asynchronous operations:

const list = Array.from(Array(3).keys())

function task (num) {
  return new Promise(function (resolve, reject) {
    setTimeout(function delay () {
      console.log(num)
      resolve()
    }, 1000)
  })
}

function queue (list) {
  Promise.all(list.map(task)).then(function () {
    console.log('Done!')
  })
}

queue(list)
// => 0
// => 1
// => 2
// => Done!

In this case we’re using Array.map() to create an array of promises, then passing that to Promise.all(iterable), which is a nifty method that allows us to call Promise.then once all the promises in the iterable array argument have been resolved.

3. ✋ Async/Await

Yes, that was pretty easy, but Promises are no longer bleeding edge enough. Let’s try using async/await!

const list = Array.from(Array(3).keys())

function task (num) {
  return new Promise(function (resolve, reject) {
    setTimeout(function delay () {
      console.log(num)
      resolve()
    }, 1000)
  })
}

async function queue (list) {
  await Promise.all(list.map(task))
  console.log('Done!')
}

queue(list)
// => 0
// => 1
// => 2
// => Done!

Here we’ve made the last step into an async function which allows us to await the results of Promise.all() before proceeding to the next line. Under the hood, async/await is just an abstraction on top of promises. The benefit is a more concise syntax which behaves like blocking code in other programming languages.

Conclusion

There are lots of different ways to deal with asynchronous operations. There is no One True Way – it’s best to be familiar with all of them and use the right tool depending on the context and what you’re trying to accomplish.

Hopefully you found this little guide helpful. Thanks for reading!