Callback is the way asynchrony is built into JavaScript. Something like, “when my dress is ready, here’s my digit, give me a call. I got a place to be”.

But then it became obvious that callback was hell and so many code fell into the pit of hell. This prompted the rise of a saviour for every future code so they don’t go to hell while developers go gaga. A promise for a better future came into the picture. This picture is what hangs on the wall of every “modern” JavaScript developer and marks the beginning of a new era for handling asynchronous flows. This gospel has been widely accepted, but peradventure there are unbelievers out there, was why I had to preach it.

No doubt, dealing with callback was hell, but promises too could be too much to deal with. Do you agree? No, right! Let’s go…

getUser(id)
  .then(user => sendSMS(user.telephone))
  .then(info => parse(info))
  .then(data => res.send(data))
  .catch(() => {})

…and even worse, what I call a promise hell.

Confession: I once wrote promises like this, before I sought for more knowledge knowing this here, doesn’t fix the issue with callbacks—not even by an inch.

I didn’t exactly know that whatever is returned from the callback passed to .then is further wrapped in a Promise and returned by the .then method. Most callbacks return void and even if they returned something, it doesn’t always matter to the code invoking them, so they just vanish. My reasoning was just like this for Promise callbacks too!

getUser(id).then(user => {
  sendSMS(user.telephone).then(info => {
    parse(info).then(data => {
      res.send(data)
    })
  })
})

That’s it! Still the same callback hell, but this time, lurking in the shadows of what was meant to be a better future—that same demon trying to possess the saviour.

With this “promise hell” up there, we’d have to do error handling for each promise separately, adding a .catch to everyone of them. Hell indeed!

Explicit promises

I termed it explicit Promise, because the intention to return a Promise would be clear even to a layman. Just like the way a function can explicitly return undefined or void and/or implicity return undefined or void when no return statement is present.

function greet() {
  console.log('Hello')
  return undefined
}

function reply() {
  console.log('Hi')
}

let greeting = greet() // undefined
let response = reply() // undefined

The intention to return undefined is clear in the first function greet, but it would take someone who is familiar with programming languages to know the function reply returns undefined too. So also with Promise.

function wait(time) {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

Our intention to return a Promise in this statement is explicity stated. There is another form which may not seem as explicit as this, but if you scrutinize quite well, it is as explicit, and TypeScript can easily make you cognizant of that.

function getUser(id: string) {
  const user: Promise<User> = fetch(`${BASE_URL}/people/${id}`).then(res => res.json())
  return user
}

If we could explore the implementation of fetch, we’d realize that it eventually returns a Promise, and since our function returns the result of fetch, we indirectly return a Promise too.

Implicit promise (async/await)

With Promise in the picture, the flow of program deviated from what you’d normally expect. Well this was nothing new, it’s what we’ve known for quite a while. Even with callbacks the flow wasn’t perfect. This is obvious from the way error handling is done in both promises and callbacks. Although it was better with promises—the separation of concerns unlike with callbacks—it’s way better with async/await. We can return to the control flow we’ve always known, try/catch.

import fs from 'fs'
import fsp from 'fs/promises'

// Error case and success case being handled from the same callback
fs.readFile(filename, (err, result) => {
  if (err) {
    return console.error(err)
  }
  // do something with result
})

// Error case and success case handled separately and clear enough
fsp
  .readFile(filename)
  .then(result => {
    // do something with result
  })
  .catch(err => {
    console.error(err)
  })

These have deviated from the traditional try/catch flow. There need to be a way we can reason about a program that would once again seem normal to us. And to be honest, the .then, .catch flow could be overwhelming when it gets too much.

We were given two brothers, async/await to relieve us the work and give us a program flow we can work with, and a clear syntax same as a normal synchronous code.

const parseJSONFile = async filename => {
  try {
    const result = await fsp.readFile(filename)
    return JSON.parse(result)
  } catch (e) {
    console.error(e)
  }
}

Now we can re-implement the previous getUser function with a better and clearer syntax. We don’t have to reach for a variable or response from only inside a callback. Our variables are available just in the same block of code.

async function getUser(id: string) {
  const res: Response = await fetch(`${BASE_URL}/people/${id}`)
  const user: User = await res.json()
  return user
}

We didn’t explicity return a promise, what we returned here is a User, but internally in the JavaScript engine, the async keyword makes the User to end up being wrapped in a Promise and look just like Promise<User>.

Promise execution order

Consider these two promises:

// Not preserved
function exec() {
  wait(4000).then(() => console.log('Promise 1'))
  wait(2000).then(() => console.log('Promise 2'))
  return Promise.resolve() // For the sake of having a similar API
}

Now consider the same promise but with async/await:

// Preserved
async function exec() {
  await wait(4000)
  console.log('Promise 1')
  await wait(2000)
  console.log('Promise 2')
}

These two implementations are different. In the first implementation, the second promise will resolve before the first. Consequently, 'Promise 2' will be logged before 'Promise 1'. In the second implementation, the order of execution is guaranteed and would be line by line as the first promise would be await-ed before any other line continues within the async function. Although the order of execution can be preserved with a .then too, but this is worth noting.

Also, the first exec function will resolve even before the two wait promises resolve, because its resultant resolution doesn’t depend on any of the wait promises. The second exec function isn’t resolved until all underlying promises have been successfully await-ed

Preserving order of execution in .then

function exec() {
  return wait(4000)
    .then(() => {
      console.log('Promise 1')
      return wait(2000)
    })
    .then(() => console.log('Promise 2'))
}

This guarantees that only after the first promise has resolved (await-ed successfully) would the second promise start execution. Also, unlike the previous one, this exec function would not resolve until all the wait promises are resolved, since the result of the wait promise is what is being returned, its resultant resolution is dependent on the overall wait promises.

The promise having a non-preserved execution order could be useful in scenarios where the execution of the promises are independent.

Be candid, which syntax seems more appealing to you, the one with await or the one with .then and return?

Converting callback-based asynchronous API to Promise

Many times you will see code trying to make a promise out a callback-based asynchronous API. Many times this is needed, cause the way a callback and a promise works differ, and we need to make things uniform and in phase. Some APIs still rely on callback for asynchrony, but this may not work for you if your code heavily depends on Promise. Reaching a callback and accessing its value, obviously can’t be done lazily with an async/await. We need to take all ammo out and summon a full-fledged Promise.

Look what we did with our wait function the other time

function wait(time) {
  return new Promise(resolve => {
    setTimeout(resolve, time)
  })
}

setTimeout is callback based, but now we can use the await or .then on it because we’ve reached into it with a promise and this gives us a uniform API if we’d been working with promises all along. Thanks to await we can have a sleep function without the hassles of doing the work we want done after sleeping in another code block or a callback. Just right after the sleep we can write the next chore to be done by JavaScript.

Working with multiple promises

Sometimes, we need to work with multiple promises and most times we may not be concerned by the fulfillment of individual promises, but their overall fulfillment or at least the fulfilment of anyone of them. Here is where some static promise methods come in.

  • Promise.all
  • Promise.allSettled
  • Promise.any
  • Promise.race

Other static methods are Promise.resolve and Promise.reject but their use case is different from the above ones

Promise.all

Returns a single promise that resolves to an array of respective values when all promises resolves and rejects when at least one of the promises rejects. Promise.all works on the all-or-nothing principle.

function wait(time) {
  return new Promise(resolve => {
    setTimeout(() => resolve(time), time)
  })
}

function wait2(time) {
  return new Promise((_r, reject) => {
    setTimeout(() => reject(new Error('Rejected')), time)
  })
}

async function exec() {
  // All promises resolves/fulfills
  // resolves to [4000, 2000]
  let p1 = await Promise.all([wait(4000), wait(2000)])
  // The promise that settles first is a rejected promise
  // The promise that settles last is a fulfilled/resolved promise
  // rejects with an Error without waiting for the last settled
  let p2 = await Promise.all([wait(4000), wait2(2000)])
  // The promise that settles first is a fulfilled/resolved promise
  // The promise that settles last is a rejected promise
  // rejects with an Error
  let p3 = await Promise.all([wait2(4000), wait(2000)])
  // All promises rejected
  // rejects with an Error
  let p4 = await Promise.all([wait2(4000), wait2(2000)])
}

Promise.allSettled

Returns a single promise that resolves to an array of objects that contains the state of all settled (resolved or rejected) promises.

interface Fulfilled {
  status: 'fulfilled'
  value: any
}

interface Rejected {
  status: 'rejected'
  resaon: any
}
async function exec() {
  let p1 = await Promise.allSettled([wait(4000), wait(2000)]) // resolves to [Fulfilled, Fulfilled]
  let p2 = await Promise.allSettled([wait(4000), wait2(2000)]) // resolves to [Fulfilled, Rejected]
  let p3 = await Promise.allSettled([wait2(4000), wait(2000)]) // resolves to [Rejected, Fulfilled]
  let p4 = await Promise.allSettled([wait2(4000), wait2(2000)]) // resolves to [Rejected, Rejected]
}

Promise.any

Returns a single promise that resolves to the first promise that resolves/fulfills and rejects with an AggregationError only when all promises rejects.

async function exec() {
  // All promises resolves/fulfills
  // resolves to 2000 without waiting for the last settled
  let p1 = await Promise.any([wait(4000), wait(2000)])
  // The promise that settles first is a rejected promise
  // The promise that settles last is a fulfilled/resolved promise
  // resolves to 4000 after the first settled rejects
  let p2 = await Promise.any([wait(4000), wait2(2000)])
  // The promise that settles first is a fulfilled/resolved promise
  // The promise that settles last is a rejected promise
  // resolves to 2000 without waiting for the last settled
  let p3 = await Promise.any([wait2(4000), wait(2000)])
  // All promises rejected
  // rejects with an AggregationError of both promises rejection
  let p4 = await Promise.any([wait2(4000), wait2(2000)])
}

Promise.race

Returns a single promise that resolves to or rejects with the first settled (resolved or rejected, respectively) promise.

async function exec() {
  // All promises resolves/fulfills
  // resolves to 2000 without waiting for the last settled
  let p1 = await Promise.race([wait(4000), wait(2000)])
  // The promise that settles first is a rejected promise
  // The promise that settles last is a fulfilled/resolved promise
  // rejects with an Error when the first settled rejects
  let p2 = await Promise.race([wait(4000), wait2(2000)])
  // The promise that settles first is a fulfilled/resolved promise
  // The promise that settles last is a rejected promise
  // resolves to 2000 when the first settled resolves without waiting for the last settled
  let p3 = await Promise.race([wait2(4000), wait(2000)])
  // All promises rejected
  // rejects with an Error when the first settled rejects without waiting for the last settled
  let p4 = await Promise.race([wait2(4000), wait2(2000)])
}

Promise.any and Promise.race may seem like they work the same but they are different—take a closer look. I promise you’ll notice the difference.