
January 08, 2024
JavaScript: Asynchronous,Promises and Async/Await
Share what you learn in this blog to prepare for your interview, create your forever-free profile now, and explore how to monetize your valuable knowledge.
What is asynchronous code?In this article, we will learn one of the important JavaScript feature that is called asynchronous JavaScript. The goal of asynchronous JavaScript is basically to deal with long-running tasks, that basically run in the background and the most common use case of asynchronous JavaScript is to fetch data from remote servers. So we will be learning about Promises, the fetch functions, async/wait and error handling. Firstly, we need to understand what is a synchronous code is?. It means that the code is executed line by line, in the exact order of execution that we define in our code. So, each line of code always waits for the previous line to finish execution. This process can create problems when one line of code takes a long time to run. For example, a particular line have alert statement, which creates a alert window and and this will block the code execution until we click OK button on the alert box. Only after that, the code can continue executing. Similarly, the execution would have to wait for example, for a five second timer to finish. In this case, nothing would work on the during the five seconds.
So, that's where asynchronous code comes into play. Let's consider following exampleconst a = 5;
setTimeout(() => {
console.log('timer reaches 5seconds');
}, 5000);
console.log('setTimeout is finished');
As you can see that, the first line of code is still synchronous and we move to the second line. However, we encountered the setTimeout() function, which will basically start a time in an asynchronous way. This means that, the timer will essentially run in the background without preventing the main code from executing. We also register a callback function, which will not be executed now, but only after the timer has finished running. So, this callback function is asynchronous JavaScript. So, a asynchronous code is executed after a task that runs in the background finishes. In this example, the task is the timer. So once the callback function is registered, we immediately move on to the next line without waiting for time to finish it's work. So a asynchronous code is non-blocking, because the rest of the code can keep running normally and when the time finishes after five seconds, the callback function will be executed. That's the big difference between synchronous and asynchronous code.
As we saw in the example, we need a callback function to implement the asynchronous behaviour, but that does not mean that callback functions automatically make code asynchronous. For example, array map() method accepts a callback function as well, but that does not make the code asynchronous. Only the certain functions such as setTimeout() work in an asynchronous way.
Promises and the Fetch API This the feature that allows us to make HTTP requests such as GET, POST, PUT, or DELETE) to a webserver. It's built into modern browsers, so we don't need additional libraries or packages to use it. Let's make a simple GET request,const request = fetch("https://restcountries.com/v3.1/name/United Kingdom")
console.log(request);
Output:
Promise {<pending>}
As you can see that, the fetch function immediately returned a promise and it says pending. Now, this promise stored in the request variable and what exactly is a promise and what we can do with it?
It's an object that is used as a placeholder for the future result of an asynchronous operation. We can also says that, the promise is a container for a future value. The future value is the response coming from an API call that we made from the fetch() function. When we start with the API call, there is no value yet, but we know that there will be some value in the future and we can use a promise to handle this future value.
let's understand the concept with the example of lottery ticket. When I buy a lottery ticket, I buy the promise that I will receive some amount of money in the future If I guess the correct outcome. So, I buy the ticket now with the prospect of winning money in the future and the lottery draw, which determines if I get the money or not, that happens asynchronously. So, I will just wait until the lottery draw happens and I will get money if I win as I was promised. So what are the advantages of using promises?- By using promises, we no longer need to rely on events and callback functions to handle asynchronous results.
- By using promises, we can chain promises for a sequence of asynchronous operations instead of nesting callback functions.
The promise Lifecycle Since, promises work with asynchronous operations, they change over time and they can be different states and this is what they call the cycle of a promise. In the beginning, a promise is pending and this is before any future value resulting from the asynchronous task is available. During this time, the asynchronous task is still doing its work in the background. When the task finally finishes, the promised is settled and there are two different types of promises, which are fulfilled promises and rejected promises. A fulfilled promise is a promise that has successfully resulted in a value as we expect it. For example, when use the promise to fetch data from an API, a fulfilled promised successfully get the data. On the other hand, a rejected promise returns an error during the asynchronous task. For example, when we fetch data from an API, if the server is down, then we would get an error like "Server is Down".
Let's go back to the analogy of the lottery ticket. This lottery draw is basically the asynchronous task, which determines the result. Then once the result is available, the ticket would be settled. Then if we guessed the correct outcome, the lottery ticket will be fulfilled. However, if we guessed wrong, then the ticket basically gets rejected.
NOTE: A promise is only settled once, which means that a promise is either fulfilled or rejected and from there the state will remain unchanged forever. These different states are relevant and useful, when we use a promise to get a result, which is called, to consume a promise. So, we consume a promise, when we already have a promise, for example, the promise that was returned from the fetch function.
Consuming Promises We will consume a promise that is returned by the fetch function. So let's implement the get country data function for fulfilled state. const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(data => console.log(data))
}
getCountryData("United Kingdom");
When we call the fetch() function, it will immediately return a promise. As soon as we start the request, the promise is still pending because the asynchronous task of getting the data, is still running in the background. As we know already, at a certain point, the promise will then be settled and either fulfilled or rejected state. To handle this fulfilled state, we can use the then() method that is available on all promises as shown in the code above, but to actually read the data from the response, we need to call the json() method on that response object. Now, this itself will also return a promise, because it is actually also an asynchronous function and if we then return that promise from this method, then basically it becomes a new promise itself. So, since this is a promise, we can again call the then() method on that. So again we have a callback and this time, we get access to the data, because the resolved value of this promise is going to be the data itself. If we log the data to console, we will be able to see the actual data from the server
So, that's where asynchronous code comes into play. Let's consider following example
const a = 5;
setTimeout(() => {
console.log('timer reaches 5seconds');
}, 5000);
console.log('setTimeout is finished');
As we saw in the example, we need a callback function to implement the asynchronous behaviour, but that does not mean that callback functions automatically make code asynchronous. For example, array map() method accepts a callback function as well, but that does not make the code asynchronous. Only the certain functions such as setTimeout() work in an asynchronous way.
const request = fetch("https://restcountries.com/v3.1/name/United Kingdom")
console.log(request);
Output:
Promise {<pending>}
It's an object that is used as a placeholder for the future result of an asynchronous operation. We can also says that, the promise is a container for a future value. The future value is the response coming from an API call that we made from the fetch() function. When we start with the API call, there is no value yet, but we know that there will be some value in the future and we can use a promise to handle this future value.
let's understand the concept with the example of lottery ticket. When I buy a lottery ticket, I buy the promise that I will receive some amount of money in the future If I guess the correct outcome. So, I buy the ticket now with the prospect of winning money in the future and the lottery draw, which determines if I get the money or not, that happens asynchronously. So, I will just wait until the lottery draw happens and I will get money if I win as I was promised.
- By using promises, we no longer need to rely on events and callback functions to handle asynchronous results.
- By using promises, we can chain promises for a sequence of asynchronous operations instead of nesting callback functions.
The promise Lifecycle
Let's go back to the analogy of the lottery ticket. This lottery draw is basically the asynchronous task, which determines the result. Then once the result is available, the ticket would be settled. Then if we guessed the correct outcome, the lottery ticket will be fulfilled. However, if we guessed wrong, then the ticket basically gets rejected.
NOTE: A promise is only settled once, which means that a promise is either fulfilled or rejected and from there the state will remain unchanged forever.
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(data => console.log(data))
}
getCountryData("United Kingdom");
Chaining Promises
We already have a small chain of promises because of the json() function in the example of getting country. In this section, we will now take chaining to a new level. For example, when we get the country details, we also need to get the data about the neighbouring country. So, the second API call depends on the data from the first call and they need to be done in sequence. Let's modify the previous example,
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(country =>{
console.log(country);
const neighbour = country[0].borders[0]
if(!neighbour) return country;
//second call
return fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
})
.then(response =>response.json())
.then(neighbourCountry => console.log(neighbourCountry))
}
getCountryData("United Kingdom");
NOTE:- then() method always returns a promise, no matter if we actually return anything or not. But if we do return a value, then that value will become the fulfilled value of the return promise. for example,
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(country =>{
return 20
})
.then(data => console.log(data))
}
getCountryData("United Kingdom");
Output
20
When we return 20 from the promise, then when we chain a new then() method, then indeed we get 20. This is because, whatever we return from the last promise that will become the fulfilled value of the next promise. This is how we are chaining the premises.
Handling Rejected Promises
To handler the errors in promises, there are two ways of handling rejections
- pass a second callback function into the then() method. So the first callback is always going to be called for the fulfilled promise and we can also pass in a second callback which will called when the promise was rejected.
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json(), error => console.log(error))
.then(country => console.log(country))
}
getCountryData("No Country");
Output
TypeError: Failed to fetch
As you can see that, we made the call offline and we did actually catch the error. However, what if the first promise was actually fulfilled and the second promise was rejected. Then we will have to pass in the a second callback in the second promise like shown below,
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json(), error => console.log(error))
.then(country => console.log(country), error => console.log(error))
}
getCountryData("United Kingdom");
- use catch() method:- Basically handling all these errors globally just in one central place. To do that, add catch() method by at the end of the chain. For example,
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(country => console.log(country))
.catch(error => console.log(error))
}
getCountryData("United Kingdom");
Besides then() and catch(), there is also the finally() method. The callback function that we defined inside finally() function will always be called whatever happens with the promise whether it is fulfilled or rejected.
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(country => console.log(country))
.catch(error => console.log(error))
.finally(() => {
console.log("Request is finished!!");
})
}
getCountryData("United Kingdom");
Output
[{...}]
0: {name: {...}, tld: Array(1), cca2: 'GB', ccn3: '826', cca3: 'GBR',...}
length: 1
[[Prototype]]: Array(0)
Request is finished!!
Throwing Errors Manually
Let's consider following example,
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>response.json())
.then(country => console.log(country))
.catch(error => console.log(error))
.finally(() => {
console.log("Request is finished!!");
})
}
getCountryData("No Country");
Output
{status: 404, message: 'Not Found'}
Request is finished!!
We get the 404 error during the fetch, which is because our API couldn't find any country with this name. Even though response returns 404 error, the fetch function still didn't reject in this case. So we will have to do it manually.
const getCountryData = (country) => {
fetch(`https://restcountries.com/v3.1/name/${country}`)
.then(response =>{
if(!response.ok){
throw new Error(`Country not found! ${response.status}`)
}
response.json()
})
.then(country => console.log(country))
.catch(error => console.log(error))
.finally(() => {
console.log("Request is finished!!");
})
}
getCountryData("No Country");
Output
Error: Country not found! 404
Request is finished!!
We create the new error using the constructor function(new Error) and then we pass in a message, which is actually the error message. Then we use the throw keyword, which will immediately terminate the current function. The effect of creating and throwing an error in any of the then() methods is that the promise will immediately reject and that rejection will then propagate to the catch() handler.
Consuming Promises with ASYNC/AWAIT
There is a better way to consume promises, which is called async/await.
const getCountryData = async (country) => {
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
const countryData = await response.json();
console.log(countryData);
}
getCountryData("United Kingdom");
We start by creating a special kind of function, which is an async function and we do this by adding async in front of the function. This function is now an asynchronous function that will keep running in the background while performing the code that inside of it, then when this function is done, it automatically returns a promise.
Inside the async function, we can have one or more await statements. This await keyword basically wait for the result of the promise. Since fetch() returns a new promise, we use await keyword with it and we just simply stored the promise into a variable called response. Now, we need to get the json out of the response, we just need to called json() on the response and store the result directly into the countryData variable. Since, json() will return a new promise, we just need to use await keyword with it.
Now, we will see how we can get the neighbouring country once we get the country.
const getCountryData = async (country) => {
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
const countryData = await response.json();
console.log(countryData);
const neighbour = countryData[0].borders[0]
if(neighbour){
const neighbourRes = await fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
const neighbourData = await neighbourRes.json();
console.log(neighbourData);
}
}
getCountryData("United Kingdom");
As you can see that, async/await is much easier to use and learn than using then() methods we used previously.
Error Handling With Try...Catch
In this section, we are going to see how error handling works with async/await. With async/await, we can't use the catch() method like we used before, because we can't attach it anywhere. Instead, we used a try/catch statement. The try/catch statement is actually used in regular JavaScript as well. We use try/catch to catch errors in async functions.
Before we do that, let's look at a simple example, just to see how try/catch work.
try{
let y = 1;
const x = 2;
x = 4
}catch(error){
console.log(error.message);
}
Output:
Assignment to constant variable.
This catch block will have access to any error occurred in the try block. Since, we have an error inside the try block, we will be able it see it in the catch block. If there is no error, then we would get no error. Similarly, we will use try/catch statement with async function to catch error. For example,
const getCountryData = async (country) => {
try{
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
if(!response.ok){
throw new Error(`Country not found! ${response.status}`)
}
const countryData = await response.json();
console.log(countryData);
const neighbour = countryData[0].borders[0]
if(neighbour){
const neighbourRes = await fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
const neighbourData = await neighbourRes.json();
console.log(neighbourData);
}
}catch(error){
console.log(error);
}
}
getCountryData("United Kingdom");
If we get any error by any of the promises, we will be able to catch that error in the catch block. Also, we can also manually throw an error as we did earlier.
Returning Values from ASYNC functions
Let's say we wanted to return value from the async function. We will continue from our previous example. Now from the getCountryData() function, we wanted to return countryData and neighbourData.
const getCountryData = async (country) => {
let countryData = null;
let neighbourData = null;
try{
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
if(!response.ok){
throw new Error(`Country not found! ${response.status}`)
}
countryData = await response.json();
const neighbour = countryData[0].borders[0]
if(neighbour){
const neighbourRes = await fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
neighbourData = await neighbourRes.json();
}
return {
countryData,
neighbourData
}
}catch(error){
console.log(error);
}
}
const data = getCountryData("United Kingdom");
console.log(data);
Output:
Promise {<pending>}
For now, let's pretend that getCountryData() is a regular function and we simple store that returned value into data variable. As you can see that, when we run the function, it returns the promise. This is because an async function always returns a promise. At this point of code, JavaScript has no way of knowing what will be returned from this function as this function is still running in the background and it is also not blocking the code. Therefore all that this function returns is a promise.
The value returned from an async function, will become the fulfilled value of the promise that is returned by the function. Since, it is promise, we know how to get the data that we want. All we need to do is to use then() method with getCountryData() function to get the fulfilled value.
const getCountryData = async (country) => {
let countryData = null;
let neighbourData = null;
try{
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
if(!response.ok){
throw new Error(`Country not found! ${response.status}`)
}
countryData = await response.json();
const neighbour = countryData[0].borders[0]
if(neighbour){
const neighbourRes = await fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
neighbourData = await neighbourRes.json();
}
return {
countryData,
neighbourData
}
}catch(error){
console.log(error);
}
}
getCountryData("United Kingdom")
.then(data => console.log(data));
Output:
{countryData: Array(1), neighbourData: Array(1)}
Now we can get the result that we want. So, we successfully returned a value from the function.
If any error occurred in the try block inside the function, we can catch by adding catch() method like we did before, but we will have to manually rethrow the error that was caught from the catch block, otherwise, catch() will not catch the error. This is because, even though there was an error in the async function, the promise that it returns is still fulfilled. By manually rethrowing an error, we will manually reject a promise that's returned from the async function So, our code will look like this,
const getCountryData = async (country) => {
let countryData = null;
let neighbourData = null;
try{
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`)
if(!response.ok){
throw new Error(`Country not found! ${response.status}`)
}
countryData = await response.json();
const neighbour = countryData[0].borders[0]
if(neighbour){
const neighbourRes = await fetch(`https://restcountries.com/v3.1/alpha/${neighbour}`)
neighbourData = await neighbourRes.json();
}
return {
countryData,
neighbourData
}
}catch(error){
console.log(error);
//Reject promise returned from the async function
throw error;
}
}
getCountryData("United Kingdom")
.then(data => console.log(data))
.catch(error => console.log(error))
However, As you can see that we mixed the old and the new way of working with promises here. Instead of having to mix them let's convert this to async/await as well. Since, await can only be used inside an async function, we can use an IIFE.
(async () => {
try{
const data = await getCountryData("United Kingdom");
console.log(data);
}catch(error){
console.log(error);
}
})();
Now, we managed to do the conversion using async/await.
Returning Promises in Parallel
Let's imagine that we wanted to get some data about three countries at the same time, but in which the order that the data arrives does not matter at all. So, we will implement as async function.
const countryData = async (country) => {
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`);
return response.json()
}
const getCountries = async (country1, country2, country3) => {
try{
const data = await Promise.all([
countryData(country1),
countryData(country2),
countryData(country3)
]);
console.log(data);
}catch(err){
console.log(err);
}
}
(async () => {
try{
const data = await getCountries("United Kingdom","United States","France");
console.log(data);
}catch(error){
console.log(error);
}
})();
Output:
(3) [Array(1), Array(3), Array(1)]
We used Promise.all() combinator function and this function takes an an array of promises and it will return a new promise, which will then run all the promises in the array at the same time.
The Promise.race() static method receives an array of promises and returns a single promise that is settled as soon as one of the input promises settles. The settled simply means that a value is available, but it doesn't matter if the promise got rejected or fulfilled.
So, in Promise.race(), the first settled promise wins the race. Let's see this in action.
const countryData = async (country) => {
const response = await fetch(`https://restcountries.com/v3.1/name/${country}`);
return response.json()
}
const getCountries = async (country1, country2, country3) => {
try{
const data = await Promise.race([
countryData(country1),
countryData(country2),
countryData(country3)
]);
console.log(data);
}catch(err){
console.log(err);
}
}
(async () => {
try{
const data = await getCountries("United Kingdom","United States","France");
console.log(data);
}catch(error){
console.log(error);
}
})();
Output:
As you can see that, we are trying to find three countries. Now these three promises will basically race against each other, like in a real race. If the winning promise is a fulfilled promise, then the value of this whole race promise is going to be the fulfillment value of the winning promise. Similarly, if the winning promise gets rejected then the rejected promise won the race. the Just keep in mind that, in Promise.race(), we only get one result and not an array of the results of all the three. From our example, the winner is "United Kingdom". We may get another winner if we run the code again because another call is going to be faster
The Promise.allSettled() static method receives an array of promises and it will simply return an array of all the settled promises. So again, no matter if the promises got rejected or not. So, it is similar to Promise.all() as it also returns an array of all the results but the difference is that Promise.all() will short circuit as soon as one promise rejects, but Promise.allSettled(), simply never short circuits. So it will simply return all the results of the all the promises.
The Promise.any() static method receives an array of promises and returns the first fulfilled promise and it will simply ignore rejected promises. So, basically Promise.any() is very similar to Promise.race(), but the difference is that rejected promises are ignored.
337 views
Please Login to create a Question