Callbacks, Promises and Async - Await

All things Asynchronous JavaScript


Callback

Callback is function passed to another function that will be executed in some time based on some condition.

Why do you need a callback

function orderPizza() {
  console.log('Pizza Order confirmed')
  console.log('Prepation started')
  let pizza
  setTimeout(() => {
    pizza = '🍕'
    console.log('Pizza prepared')
  }, 2000)
 
  return pizza
}
 
console.log('Order Pizza')
let pizza = orderPizza()
console.log(`Eat ${pizza}`)
/* OUTPUT
Order Pizza
Pizza Order Confirmed
Prepation started
Eat undefined
(after 2sec)
Pizza Prepared
*/

Well you can't really eat undefined. Let's use callbacks to solve this problem.

function orderPizza(callback) {
  console.log('Pizza Order confirmed')
  console.log('Prepation started')
  let pizza
  setTimeout(() => {
    pizza = '🍕'
    console.log('Pizza prepared')
    callback(pizza)
  }, 2000)
}
 
function eatPizza(pizza) {
  console.log(`Eat ${pizza}`)
}
 
console.log('Order Pizza')
orderPizza(eatPizza)
console.log('call my friend while I wait for the 🍕')
/* OUTPUT :
Order Pizza
Pizza Order Confirmed
Prepation started
call my friend while I wait for the 🍕
(after 2 sec)
Pizza Prepared
Eat 🍕
*/

Another good common example of callback used for async code is using an event listener.

document.addEventListner('click', () => {
  console.log('Why do you click me????')
})

Here the callback function is waiting for the user to click the document and only then it executes.

Problem with using callback

Say that we have 3 functions that needs to be executed only after the other one finishes and each of them take time does not run on the main thread (Introduces callback hell)

loadDataFromAPI1(function (result1) {
  loadDataFromAPI2(function (result2) {
    var combinedResult = {
      data1: result1,
      data2: result2,
    }
    saveDataToDatabase(combinedResult, function (savedResult) {
      console.log('Saved:', savedResult)
    })
  })
})
 
// Another Example
 
setTimeout(() => {
  console.log('first')
  setTimeout(() => {
    console.log('second')
    setTimeout(() => {
      console.log('third')
    }, 1000)
  }, 1000)
}, 1000)

Promises

We can achieve the same result with much more cleaner syntax using promises.

There are always two parts to a promise, one is the promise maker and the other is the receiver (just like in real like promises)

Using callback to fetch the current weather:

function getWeather(callback) {
  setTimeout(() => {
    callback('Sunny')
  }, 3000)
}
 
function weatherRender(data) {
  let weather = data
  document.body.innerText(weather)
}
 
getWeather(weatherRender)

Now Let's convert this in a Promise code

promiseMaker.js
function getWeather() {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      resolve('Sunny') // reject('error')
    }, 2000)
  })
}

The getWeather() returns a promise, since all functions needs to return something otherwise it would return undefined

Its like the getWeather() saying, hey I don't have the weather yet but I promise to you that I will get you (fulfill) the data once I have it.

The Promise object takes a function and that is where all the async code goes. This function takes in 2 arguments resolve and reject, which is called if the promise is fulfilled or is failed respectively.

Now let's consume the promise using the receiver

consumePromise.js
const promise = getWeather()
promise
  .then((msg) => {
    console.log(msg) // Sunny
  })
  .catch((msg) => {
    console.error(msg) // error
  })

Now the nice thing about using Promise instead of callback is :

Say we now want to get a Icon based on the weather :

getWeather()
  .then(getWeatherIcon)
  .then((msg) => console.log(msg))
 
function getWeatherIcon(weather) {
  return new Promise((resolve, reject) => {
    switch (weather) {
      case 'Sunny':
        resolve('🌞')
        break
      case 'Cloudy':
        resolve('⛅')
        break
      case 'Rainy':
        resolve('🌧️')
        break
      default:
        reject('No icon found')
    }
  })
}

So now instead of getting into a callback hell, we are just chaining the functions together and we can go as far as we like.

Another benefit of this approach is that you have the catch function to catch some errors where as in the callback case you would have to pass 2 callback function to handle the success and error.

As an exercise, try to dry-run the following code and reason what the output will be:

exercise.js
const promise = new Promise((resolve, reject) => {
  let sum = 0
  console.log('loop started')
 
  for (let i = 0; i < 100; i++) {
    sum += i
  }
  console.log('loop ended')
 
  console.log(sum)
  if (sum == 4950) resolve('Success')
  else reject('Error')
})
 
promise.then((msg) => console.log(msg)).catch((msg) => console.error(msg))
 
console.log('first')

There's also a finally block apart from then & catch, which runs at the end no matter if the promise is fulfiled or rejected. Can use it to remove some event listeners and clear things up.

In case of chaining if you want to handle the errors of each one of the promise separately you can do that by passing a second param to the then function.

func1().then(func2, onError1).then(onSucces, onError2)
 
// Or you can handle all the errors at the end
func1()
  .then(func2)
  .then((msg) => console.log(msg))
  .catch(onError)

Dealing with multiple Promises

Function like Promise.all,Promise.any, Promise.race, Promise.allSettled takes in an array of promises and returns a single promise.

Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
  .then((messages) => {
    console.log(messages)
  })
  .catch((error) => {
    console.error(error)
  })
 
// [1, 2, 3]
 
Promise.all([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)])
  .then((messages) => {
    console.log(messages)
  })
  .catch((error) => {
    console.error(error)
  })
 
// 2

The then function is only called if all the promises are resolved, else calls the catch function.

Another Example: 🌟

const urls = [
  'https://jsonplaceholder.typicode.co/users', // wrong url
  'https://jsonplaceholder.typicode.com/posts',
  'https://jsonplaceholder.typicode.com/albums',
]
 
Promise.all(
  urls.map((url) => {
    // returns a promise for each url
    return fetch(url).then((resp) => resp.json())
  })
)
  .then((results) => {
    console.log(results[0])
    console.log(results[1])
    console.log(results[2])
  })
  .catch(() => console.log('err'))
// Only err gets printed

Other methods:

promise.any.js
Promise.any([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)])
  .then((message) => {
    console.log(message)
  })
  .catch((error) => {
    console.error(error)
  })

Promise.any prints the 1st promise that is resolved.

To get the 1st promise that finishes (no matter if it fails or resolves) use race

promise.race.js
Promise.race([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)])
  .then((message) => {
    console.log(message)
  })
  .catch((error) => {
    console.error(error)
  })
// 1

There is also a allSettled that waits for each promise to finish and then prints the status of each

promise.allSettled.js
Promise.allSettled([Promise.resolve(1), Promise.reject(2), Promise.resolve(3)])
  .then((message) => {
    console.log(message)
  })
  .catch((error) => {
    console.error(error)
  })
 
/* 
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 },
  { status: 'fulfilled', value: 3 }
]
*/

Notice in this case only then is ran no matter what(even if all the promises are rejected), catch is not called

There is also finally function that run in all case, where then runs or catch, finally runs for sure

let promise = Promise.resolve('here')
 
promise
  .then((msg) => {
    console.log(msg)
  })
  .catch((error) => {
    console.error(error)
  })
  .finally(() => {
    console.log('I will run always')
  })
// here
// I will run always
 
let promise = Promise.reject('error')
 
promise
  .then((msg) => {
    console.log(msg)
  })
  .catch((error) => {
    console.error(error)
  })
  .finally(() => {
    console.log('I will run always')
  })
// error
// I will run always

Async & Await

Async & Await are just syntactical sugar to write asynchronous code that feels more like synchronous code.

asyncAwait.js
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(450)
    }, 10)
  })
}
 
// Using Promise in the receiver
function start1() {
  getData().then((res) => console.log(res))
}
 
// Using the async, await syntax
async function start2() {
  const res = await getData()
  console.log(res)
}

Few key points

  1. async and await are used together (Except for in JS Modules and Chrome Dev Tools, where you can you await without async)
  2. They are used only on the receiver side and the promise maker implementation stays the same
  3. You can await any function that returns a Promise. And hence you can use it with fetch API as well
  4. Any function can be converted to async, using the async keyword
  5. All async functions always returns a Promise
  6. Use try / catch and finally blocks to handle errors & perform clean ups

Top level Await

test.mjs
fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then((res) => res.json())
  .then((json) => console.log(json))
 
console.log('test')
 
/* Output
test
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
*/
 
// async await version
let res = await fetch('https://jsonplaceholder.typicode.com/todos/1') // using top level await (node)
let json = await res.json()
console.log(json)
 
console.log('test')
 
/* Output 
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
test
*/

Why did we get different outputs in each case?

Usually await is not allowed outside a async function, since in that case the async function bounds the code that will not be executed, until we receive the response. When using top level await, entire module acts as a async function, and all the code below is ran after the code with await keyword, since JS has no idea when will the await response will be used.

Which is not the case with promise version since JS knows that it will only have to wait till the chained then(), catch(), or finally() functions, and all the other code below it can run without waiting for it. And thus diffrent result.

For better undertanding please evalutate the three cases defined below:

async function foo() {
  let res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  let json = await res.json()
  console.log(json)
}
foo()
 
console.log('test')
 
/* 
test
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
*/
 
async function foo() {
  console.log('before test')
  let res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  let json = await res.json()
  console.log(json)
}
foo()
 
console.log('test')
 
/* 
before test
test
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
*/
 
async function foo() {
  let res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  console.log('before test')
  let json = await res.json()
  console.log(json)
}
foo()
 
console.log('test')
 
/* 
test
before test
{ userId: 1, id: 1, title: 'delectus aut autem', completed: false }
*/

Find the output

exercise1.js
function getWeather() {
  console.log('getWeather function start')
  return new Promise((resolve, reject) => {
    console.log('Promise object start')
    setTimeout(() => {
      resolve('Sunny')
      console.log('Inside the timeout')
    }, 2000)
    console.log('After the timeout')
  })
}
 
console.log('Before the function call')
getWeather()
  .then((msg) => console.log(msg))
  .catch((error) => console.error(error))
console.log('After the function call')

Once more:

exercise2.js
function getWeather() {
  console.log('getWeather function start')
  return new Promise((resolve, reject) => {
    console.log('Promise object start')
    setTimeout(() => {
      resolve('Sunny')
      console.log('Inside the timeout')
    }, 2000)
    console.log('After the timeout')
  })
}
 
async function logWeather() {
  console.log('logWeather start')
  const result = await getWeather()
  console.log(result)
  console.log('logWeather middle')
  console.log(result)
  console.log('logWeather end')
}
 
logWeather()

Another one: 🌟

exercise3.js
function getWeather() {
  console.log('getWeather function start')
  return new Promise((resolve, reject) => {
    console.log('Promise object start')
    setTimeout(() => {
      resolve('Sunny')
      console.log('Inside the timeout')
    }, 2000)
    console.log('After the timeout')
  })
}
 
async function logWeather() {
  console.log('logWeather start')
  const result = await getWeather()
  console.log('logWeather middle')
  console.log(result)
  console.log('logWeather end')
}
 
function sayHi() {
  console.log('Hi Mom!')
}
 
function sayBye() {
  console.log('Bye Mom!')
}
 
logWeather()
sayHi()
sayBye()