Callbacks, Promises and Async - Await
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
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
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:
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([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([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([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.
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
async and await
are used together (Except for in JS Modules and Chrome Dev Tools, where you can you await withoutasync
)- They are used only on the receiver side and the promise maker implementation stays the same
- You can
await
any function that returns aPromise
. And hence you can use it withfetch
API as well - Any function can be converted to
async
, using theasync
keyword - All
async
functions always returns aPromise
- Use
try / catch
andfinally
blocks to handle errors & perform clean ups
Top level Await
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
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:
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: 🌟
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()