When it comes to Promises and Async await which are closely related concepts in Javascript, people are always confused even after watching tutorials or reading numerous articles about them. Well, worry not because I assure you if you read this article thoroughly you will have a better understanding of what they are and how they work.
The Evolution of Asynchronous programming
Callback functions --> Promises --> Async await
Don't worry, we will understand what this evolution is all about and why it took place, further in the article.
Javascript is synchronous, or is it?
Javascript is a synchronous language, meaning that every statement is run line by line in a sequential manner, and only when a line of code is done being executed is when the next line will run.
Now, what if something takes a long time to execute than usual, and it blocks the whole program? This is bad, we don't want to slow down our program just because a single line of code (which might be an API call to get an image resource) is blocking our entire script. That is why we needed asynchronous code, which does not block the code after it, and can run in the background. There are a lot of ways we can do asynchronous programming in JavaScript: callback functions, Promises and Async await.
Callback functions
Callback functions are basically functions passed to functions as arguments to be executed later, and they can be synchronous or asynchronous. One most popular example of this is the setTimeout function.
setTimeout(function() {
console.log('Hello World!');
}, 500);
This code does a console.log after 500 milliseconds and runs asynchronously. A synchronous callback function might look like this:
let numbers = [1, 2, 4, 7, 3, 5, 6]
numbers.sort((a, b) => a - b)
console.log(numbers); // [ 1, 2, 3, 4, 5, 6, 7 ]
The function sort() has an anonymous arrow function that works as a callback to sort the array, and we print it on the console. Notice that the arrow function runs before the console and therefore we get the sorted array, but if you would have sorted the same array with a setTimeout then the console will print unsorted array:
let numbers = [2, 1, 4, 7, 3, 5, 6]
// numbers.sort((a, b) => a - b)
setTimeout((a,b) => {
numbers.sort((a,b) => a-b)
// console.log(numbers)
}, 0)
console.log(numbers); // [2, 1, 4, 7, 3, 5, 6]
This is because asynchronous code runs after all synchronous code is finished executing, that is why even though the setTimeout had 0 wait time, it still ran after the console.log, even though console.log comes later in code.
Callbacks are good for asynchronous programming as long as you have very few async actions, but if you want to run more functions asynchronously, then callbacks become an ugly mess!
Imagine if you have a number of requests and you want to run them in callback functions. The code might look like this:
const url1 = 'https://fakeapi.com/1/'
const url2 = 'https://fakeapi.com/2/'
const url3 = 'https://fakeapi.com/3/'
function callback(url,msg){
// calls api here
console.log(msg)
}
setTimeout(() => {
callback('first message')
setTimeout( () => {
callback('second message')
setTimeout( () => {
callback('third message')
},0)
}, 0)
},0)
// first message
// second message
// third message
These nested callbacks create what is infamously called the Callback Hell, and this type of code can become unmaintainable very quickly. To solve this, Promises were introduced in ES6( Ecmascript 2015).
All these Promises you do
Promises in JS are like promises in real life, it either is resolved or never completed. There are certain things you need to know to understand Promises better. A Promise can be in one of the three states:
- resolved
- rejected
- pending
Promise objects can be made by the Promise() constructor, which takes in an executor function, which in turn takes two functions as parameters: a resolve function and a reject function. This is how you make a new promise:
const aPromise = new Promise( (resolve, reject) => {
resolve("promise resolved")
})
aPromise.then(( value) => {
console.log(value)
})
.catch( (error) => {
console.log(error)
})
This "executor" function terminates with the invocation of resolve or reject function depending on the value of the Promise, meaning that the executor function runs some code, and calls resolve function if the Promise gets resolved or reject function if it throws an error.
After your promise is resolved or rejected you can chain it with the methods .then() and .catch() respectively. These methods themselves return Promises. You can also chain multiple .then() methods to modify the value that comes back from a resolved Promise:
aPromise.then(( value) => {
return value = 'changed value'
})
.then((value) => {
console.log(value)
})
We usually use these methods while making an api call. Let's use the fetch API to fetch list of Pokemon from the PokeAPI. The fetch(). function returns a Promise so we can attach our .then() and .catch() methods:
const url = 'https://pokeapi.co/api/v2/pokemon?limit=100&offset=200'
fetch(url)
.then((response) => {
response.json()
})
.then((data) => {
console.log(data)
})
This will give us a list of 100 Pokemon with other properties. Promises did make our lives easier but our code still looks like it's bulky with all these chaining methods. Async await made this much more cleaner and allowed us to write asynchronous code that looked very much like our good old synchronous code.
Async await
Remember that Async await is kinda like Classes in Javascript, it might look like we are writing entirely different code, but under the hood, we are still using Promises. It's just that the syntax and readability got better.
Async await is used in place of .then() and .catch() when working with Promises. We need to assign a function ASYNC and then inside of it we can have the await keyword before the asynchronous actions we'd like to perform ( meaning the code we think might take longer time). Like this:
const loadData = async () {
const url = 'https://jsonplaceholder.com/todos/1/'
const res = await fetch(url)
const data = res.json()
console.log(data)
}
loadData()
Can you see the difference between this and Promises? This is so much cleaner and doesn't even look like we are doing something asynchronously. Just define an async function and add await before anything you'd want to run asynchronously.
Using async on a function, with await inside on any operation( like an API call) will say to JS : "Hey man just don't return this function until this thing with the await is done, after that do whatever you gotta do"
Things to remember about Async await:
- always assign the function you want to have asynchronous code in as an async function.
- you can only use await inside of an async function.
- An async function returns a Promise, so you can chain multiple async functions.
try-catch block
Just like the .catch() block we used in Promises, we use try-catch for error handling in Async await. This makes the code much cleaner and more organized.
const loadData = async () {
try{
const url = 'https://jsonplaceholder.com/todos/1/'
const res = await fetch(url)
const data = await res.json()
console.log(data)
}
catch(err){
console.error(err)
}
}
Async functions also return a promise. So get this, a fetch or an Axios request returns a promise, an async function also returns a promise. We can either do a .then() chaining or another async await on a Promise.
Promise.all()
If you need to make multiple requests to an API, and you go by the approach of async await, it will take the sum of time it takes for all requests to return data. So let's say each request takes 1 sec, and for 3 requests, we will have to wait 3 secs if we just await on every single request like this:
/* Wrong approach */
const loadData = async() {
try(){
const url = 'https://jsonplaceholder.com/todos/1/'
const url2 = 'https://jsonplaceholder.com/todos/2/'
const url3 = 'https://jsonplaceholder.com/todos/3/'
const res = await fetch(url)
const res2 = await fetch(url2)
const res3 = await fetch(url3)
const data = await res.json()
const data2 = await res2.json()
const data3 = await res3.json()
return [data, data2, data3]
}
catch(err){
console.error(err)
}
}
With Promise.all(), each request runs parallel to each other, resulting in a much quicker response.
/* Right approach */
const loadData = async() {
try(){
const url = 'https://jsonplaceholder.com/todos/1/'
const url2 = 'https://jsonplaceholder.com/todos/2/'
const url3 = 'https://jsonplaceholder.com/todos/3/'
const results = await Promise.all([
fetch(url),
fetch(url2),
fetch(url3)
])
const dataPromises = results.map( res => res.json())
const finalData = await Promise.all(dataPromises)
return finalData
}
catch(err){
console.error(err)
}
}
( async() => {
const data = await loadData()
console.log(data)
})()
That's all. We learned a lot of things, what is asynchronous programming, how do callback functions work, about Promises and Async await.
For further reading:
- javascript.info/async
- developer.mozilla.org/en-US/docs/Web/JavaSc..
- developer.mozilla.org/en-US/docs/Web/JavaSc..
Thanks for reading!
If you like my work you can support me on buymeacoffee.com/rishavjadon