Asynchronous Programming In JavaScript

Asynchronous Programming In JavaScript

JavaScript is a programming language that can only run one line of code at a time. This implies that JavaScript is a single-threaded language, and by default, it runs code in a synchronous manner.

Asynchronous programming was introduced to help handle tasks that could not be executed synchronously. It allows multiple tasks to run simultaneously and is crucial in creating a lot of functionalities in websites and digital applications.

In this article, you will learn all that you need to know about asynchronous programming in JavaScript, but first, let’s compare synchronous programming and asynchronous programming.

Synchronous vs Asynchronous programming

Synchronous programming is a type of programming in which code is executed line by line, and it is impossible to skip a certain line of code. Let’s quickly take a look at a very basic example of synchronous programming:

let valOne= "harry";

 let valTwo= "potter";

console.log(valOne); //returns harry

 console.log(valTwo); //returns potter

As you can see, the code is quite straightforward. valOne was the first to be logged out, and then it moved on to logging out valTwo.

Asynchronous programming can get around JavaScript’s default nature of running code synchronously. It allows us to run multiple lines of code at once, and this allows time-consuming tasks to run in the background, while other tasks run without any delay.

Here’s an example of asynchronous programming:

let valOne= "harry";
let valTwo= "potter";

console.log(valOne); //returns harry


setTimeout(function() {
    console.log("slow"); //returns slow
}, 6000);

console.log(valTwo); //returns potter

When we run the code above, the first variable to be logged out is valOne, followed by valTwo. Only after 6 seconds, as specified by the setTimeout() method, will the string “slow” be logged out onto the screen. In case, you are confused by the time conversion, time in the setTimeout() method is measured in milliseconds (ms) and 1000 ms equals one second.

Callbacks

A callback is a function that serves as an argument to another function. The callback function is usually executed after an event has been triggered. Callbacks are classified into two types:

  • Synchronous: It executes immediately and blocks code until it completes before moving on to the next task.

  • Asynchronous: It executes after a task’s completion, allowing the program to continue without waiting for the task to be completed.

Let’s take a look at an example of a callback:

function sum(x, y, callback) {
    let result= x+y;
    callback(result);
}

function displayValue(answer) {
    console.log(answer); //returns 4
}

sum(2, 2, displayValue);

In the sum function, we used the displayValue function as an argument for the callback parameter, and set a value of 2 as an argument for both the x and y parameters. In the sum function, we added the values of the arguments for the x and y parameters and stored the answer in the result variable. The answer was then passed as an argument to the callback function. When we run the code above, the displayValue function will log out a value of 4 onto the screen.

Callback Hell

Callback hell is basically the nesting of callbacks within callbacks until it reaches a point, where code becomes unreadable and hard to understand. Callback hell is commonly known as the “pyramid of doom” because of its pyramidal structure.

Let's take a look at an example of a callback hell:

function message1(callback) {
    setTimeout(function() {
        console.log("first trumpet is blown");    
     }, 1000);
};
function message2(callback) {
    setTimeout(function() {
        console.log("second trumpet is blown");    
     }, 1000);
};

function message3(callback) {
    setTimeout(function() {
        console.log("third trumpet is blown");    
     }, 1000);
};

function hellCode() {
    console.log("raise your trumpets");
    message1(function() {
        message2(function() {
            message3(function() {
                console.log("stop blowing")
                });
           });
     });
};

The code above shows that each function inside the hellCode function is a callback, which is nested within the other callbacks, and they are executed at 1-second intervals. These nested callbacks are executed in the order of their hierarchy within the nest, so message1 gets executed first, followed by message2 , and lastly, message3. If we add more functions to these nested callbacks in the hellCode function, the code will become harder to understand, and that is what they refer to as “callback hell.”

Promises

Promises help us know if an asynchronous operation succeeded or failed, and they return a value/error based on the outcome. Promises are alternatives to callbacks because they help to avoid callback hell by providing a cleaner syntax for the nesting of functions.

JavaScript promises can be likened to real-life promises, whereby we promise to do a task, and when we succeed in carrying out that task, we will have resolved/completed that task; otherwise, we will have failed woefully. They can be used to handle network requests, file operations, and other asynchronous tasks.

A promise can be in 3 states:

  • Pending: The initial state, when the asynchronous operation is in progress.

  • Resolved: The state, when the asynchronous operation has successfully completed.

  • Rejected: The state, when the asynchronous operation fails.

Let's take a look at a basic example of a promise:

let quiz= new Promise(function(resolve, reject) {
     let result= 2 + 2;
     If(result == 3) {
         resolve("correct");
     }else{
         reject("wrong");
     };
});

quiz.then((message) => {
    console.log(message);
}).catch((err) => {
    console.log(err);
});

Now, let’s do an in-depth analysis of the code above. We know that promises are JavaScript objects and therefore, they can be created using an object constructor, new Promise(). Once we create a promise, it takes a single parameter, which is a function with its own default parameters, resolve, and reject.

In the code, we stored the promise in the quiz variable. We defined a variable named result, that stores the answer to our basic arithmetic operation, and this represents the task attached to the promise. Using the if/else statements, if the result variable was equal to 4, the promise would have been resolved; otherwise, the promise would have been rejected.

To access the values held in the promise’s resolve() and reject() methods, we used the then() method to consume the promise. It should be noted that the then() method will only function if the promise is resolved. We used the then() method to log out the value contained in the resolve() method’s parenthesis, and this means the message parameter returns the string “correct.” When we use multiple then() methods, this results in the chaining of promises.

In case the promise failed/rejected, we have to use the catch() method to handle the error from the rejected promise. The catch() method only works with the reject() method. We used the catch() method to log out the value contained in the reject() method’s parenthesis. So, the err parameter returns the string "wrong.”

The code inside the finally() method will run regardless of the outcome of the promise. It is mostly used for cleanup tasks that are necessary regardless of the promise’s outcome.

Promise Methods

In this section, we will explore some promise methods that can be very useful for your next coding project.

Promise.then()

This allows us to return a value if an asynchronous task has been completed.

Promise.catch()

If an asynchronous task fails, the catch() method returns an error message to indicate the failure.

Promise.finally()

The condition of its execution does not depend on the success or failure of a promise.

Promise.all()

This fulfills when all the promises in a function are fulfilled, and it automatically rejects if one of them fails. Let’s take a look at an example using the all() method:

let valOne= new Promise((resolve, reject) => { resolve("peter"); });

let valTwo= new Promise((resolve, reject) => { reject("parker"); }); 

Promise.all([valOne, valTwo]).then((values) =>
console.log(values)).catch((err)=> { console.log(err) });

There are two promises in the code above. The promise was resolved in valOne and rejected in valTwo. Instead of using both the resolve() and reject() methods, you can create a default outcome. When we run the code above, it logs out “parker” as the error message because valTwo was intentionally rejected. If we had decided to resolve valTwo, an array of values representing the strings in both promises’ resolve() methods would have been logged out onto the screen.

Promise.race()

This is a method that fulfills after the fastest promise succeeds.

Let’s take a look at an example that illustrates the use of race() method:

let valOne= new Promise((resolve, reject) => { setTimeout(resolve, 5000, "harry"); });

 let valTwo= new Promise((resolve, reject) => { setTimeout(resolve, 1000, "potter"); });

promise.race([valOne, valTwo]).then((values) => { console.log(values)});

We created two promises that make use of the setTimeout() method. After 5 seconds, valOne will be resolved, but valTwo resolves only after a second. When we run this code, the race() method will only return potter” because valTwo took the least time to execute.

Promise.allSettled()

This fulfills when all promises are settled.

Let’s take a look at an example that involves the use of allSettled() method:

let valOne= new Promise((resolve, reject) => { setTimeout(resolve, 5000, "harry"); });

let valTwo= new Promise((resolve, reject) => { setTimeout(resolve, 1000, "potter"); });

Promise.allSettled([valOne, valTwo]).then((values) => { console.log(values)});

When we run the code above, it will return an array of objects, which contains the status and value properties, which will show whether the promise was fulfilled or rejected and return the value contained in a promise’s resolve() or reject() method, respectively.

Promise.any()

This works contrary to the all() method because it fulfills when any of the promises are fulfilled and rejects only when all of the promises are rejected.

Let’s take a look at an example of how to use the any() method:

let valOne= new Promise((resolve, reject) => {setTimeout(resolve, 5000, "harry") });

let valTwo= new Promise((resolve, reject) => { setTimeout(reject, 1000, "potter") });

Promise.any([valOne, valTwo]).then((value) => {console.log(value)});

The any() method ensures that “harry” is logged out because valOne is resolved.

Chaining of Promises

When you chain promises together, you are simply passing the outcome of a promise as input to the next promise. As a result, if the first promise fails, the remaining promises will also fail.

Multiple then() methods are used to link promises, and they all use a callback function as an argument, which returns a new promise. After this new promise has been resolved, the value returned can then be accessed by the next promise in the chain.

Let’s take a look at an example of promise chaining:

fetch('https://jsonplaceholder.typicode.com/posts')
.then((response) => response.json())
.then((data) => {console.log(data)})
.catch((err) => {console.log(err)});

The first promise was to fetch data from a server using Fetch API. The second promise was the parsing of the response as JSON. The last promise was to log out the parsed data from the server. The catch() method was used to catch any error encountered during the fetch request.

How the Fetch API Relates to Promises

The Fetch API is a modern API that provides a very simple way of making asynchronous HTTP requests in JavaScript. It can be used to interact with web servers, and it can be used to retrieve JSON data, XML, and images. It makes use of promise chaining to access and manipulate data obtained from a data server.

Due to its straightforward approach, it is a much better alternative to XMLHttpRequest, which is the old way of executing asynchronous HTTP requests.

Let’s take a look at an example of how the Fetch API can be used to make HTTP requests:

fetch('https://www.boredapi.com/api/activity')
.then((response) => response.json())
.then((data) => {console.log(data)})
.catch((err) => {console.log(err)});

This is similar to the code in the preceding example. We can see from the code above that the promise’s main task is to fetch data from a server using the Fetch API. When the Fetch API succeeds in fetching the data, we use the then() method to parse it as JSON. A second then() method chained to the first then() method then acts on the parsed data by logging it out onto the screen. The catch() method is used to handle any error encountered during the fetch request.

Async/await

When you are chaining promises, the chain might become very long due to the use of several then() methods, and this is where async/await comes in handy. It will complete the asynchronous task carried out by a promise with fewer lines of code.

async is a keyword that is used to make a function asynchronous, while await is a keyword that is used in an asynchronous function to ensure that the function only runs after a promise’s completion.

Let’s take a look at an example of how to use async/await:

async function getPost() {
     const response= await fetch('https://jsonplaceholder.typicode.com/posts/1');
     const data= await response.json();
     console.log(data);
};
getPost();

Looking at the code above, the async keyword was used to make the getPost function asynchronous. In the getPost function, we used the await keyword to wait for the fetch function to get data from an API. Once that action was completed, we used the await keyword again to parse the data as JSON. The data was then logged out onto the screen. It is important that we call the asynchronous function, which in this case happens to be the getPost function.

Error Handling With try…catch()

We can use a try…catch block for an asynchronous function via async/await. This is similar to the then() and catch() method used in promises. The try block contains the asynchronous code, and once an error arises, the catch block helps with error handling.

Let’s take a look at an example of how to use the try…catch block:

async function getPost() {
    try{
       const response= await       fetch('https://jsonplaceholder.typicode.com/posts/1');
        if(response.status !== 200){
            throw new Error(“failed”)
        }
         const data= await response.json();
         console.log(data);
     } catch(err){
       console.log(err);
     };
};
getPost();

Slight alterations were made to the getPost() function in the previous example, and we put the asynchronous code in the try block. Using an if statement, we put a condition that states that if the HTTP status code wasn’t 200, the code would throw an error. In case you are unfamiliar with HTTP status codes, 200 signifies that the request was successful. When there is an error, our custom error will then be passed into the catch block as the err parameter and logged out onto the screen.

Conclusion

You must understand asynchronous programming because you will encounter it on almost every coding project. Once you master asynchronous programming in JavaScript, the sky will be your limit. Keep practicing, and don't let the job market discourage you.