Thursday, March 28, 2024

Asynchronous Programming in JavaScript

JavaScript is a single-threaded programming language, which means only one task can be executed at a time. As there is only a single thread of execution then a question arises: how do developers execute a long-running task without impacting the thread of execution? Well, the answer is simple: asynchronous programming.

Asynchronous programming in JavaScript offers a great set of features for handling input/output (IO) operations that are immediately executed and have no immediate response. Most applications these days require asynchronous programming in some way. Making AJAX (Asynchronous JavaScript and XML) requests across the network is one good use-case of asynchronous programming in JavaScript.

In the next section we will discuss how asynchronous programming affects the development process.

Single Thread Processing in JavaScript

JavaScript runs on a single thread, which means when it is executed in the browser, everything else stops; this is necessary because changes to the DOM cannot be performed on parallel threads. Consider a situation where one thread is appending child elements to the node and the other is redirecting a URL – that is not an ideal scenario.

JavaScript handles the processing of tasks very quickly in small chunks, so most users rarely notice it. For example, listening for a button click event, running the calculation, and then updating DOM – all of that is performed behind the scene, hidden from the user. Once that operation is completed, then the browser is free to pick a new task from the queue. The user, in this instance, is not aware the queue exists at all.

JavaScript Asynchronous Programming and Callbacks

Single thread processing is not well-suited for many situations. Consider a scenario where JavaScript encounters a slow process such as performing data operations on a server or making AJAX requests. These operations could take several seconds or even minutes; the browser will be locked while it waits for the response. In the case of NodeJS, the application will not be able to process further requests.

In JavaScript, the solution is asynchronous programming. Rather than waiting for an operation to complete, a process would call another process (such as a function) when the result is ready. That is a process known as Callback; it is passed to any asynchronous function as an argument. The following is a n example of using Callbacks in asynchronous JavaScript code:

 
doCalculationAsync(callback1);
console.log('Process finished');
// call when doCalculationAsync completes
function callback1(error) {
  if (!error) console.log('Calculation completed');
}
 

Here, doCalculationAsync() accepts a callback function as a parameter; callback1 is going to execute at some point in time in the future, irrespective of how long doCalculationAsync() will take. Below is the output of this code once executed:

Process finished
Calculation completed

What is Callback Hell?

Consider the following code:

asyncFunction1((err, res) => {
  if (!err) asyncFunction2(res, (err, res) => {
    if (!err) asyncFunction3(res, (err, res) => {
      console.log('async1, async2, async3 completed');
    });
  });
});

As you can see, the above code is hard to understand and manage; it may get even worse when error-handling logic is added to it. This is what is known as callback hell.

On the client-side, callback hell is relatively rare and can go up to three levels deep if you are making AJAX calls or updating DOM. Fortunately, however, callback hell is manageable.

The situation gets a little bit more complicated with server-side applications. For instance, a NodeJS application could be dealing with receiving file uploads, writing logs, or making further API calls. Callback hell can really be problematic in such situations.

Modern Javascript comes up with an approach to solve this problem called Promises.

Preventing Callback Hell with Promises

Promises were introduced in ES6. Promises are treated as wrapper functions around callback functions. They allow us to handle the asynchronous operations in a more manageable way. They act as a placeholder for the values that are not known at the time of creation. Users can attach a handler for a successful operation or a reason for failure. In this way, asynchronous methods return values as if they were synchronous.

To use Promises, we make use of the Promises constructor. It receives two parameters: resolve and reject – both of these are callbacks. We can run any async operations within the callbacks then resolve them if they are successful, or reject them if there is a failure.

Promises can be in one of the three possible states:

  • Pending: Promise has not yet begun operation
  • Fulfilled: Promise resolves after completion of the operation
  • Rejected: Promise fails to complete its operation

Here is an example showing JavaScript Promises in code:

const promise = new Promise((resolve, reject) => {
    if (success) {
      resolve('async action successful!')
    }
    else {
      reject(throw new Error('Something happened. Couldn’t complete async action'))
  }
})

This function returns a new promise, which would be initiated with a pending state. Here, resolve and reject are callbacks. When a promise is resolved with success, then it is said to be in the fulfilled state, while, on the other hand, if the promise fails to perform an operation or returns an error then it is said to be in a rejected state.

Let’s make use of the promise we have created above:

promise.then((data) => {
  console.log(data) // async action successful!
}).catch((error) => {
  console.log(error) // Something happened. Couldn’t complete async action
}).finally(() => {
  console.log('Run when the promise must have settled')
})

Note: the finally() block here is used for code cleanup purposes only. It handles other stuff when the promise has completed its operations.

Read: Asynchronous Programming in ECMAScript.

Promise Chaining in JavaScript

One of the advantages of using promises is the fact that they can be chained. It is possible to chain a couple of them together to run additional asynchronous tasks, one after the other. Below is an example illustrating how to chain promises in JavaScript:

asyncOperation('http://localhost:8080')
  .then(asyncGetSessionDetails)  
  .then(asyncGetUserDetails)      
  .then(asyncLoginAccess)    
  .then(result => {        
    console.log('action completed');  
    return result;            
  })
  .catch(error => {             
    console.log('error occured', error);
  });

Problems with Promises

Promises solve the problem of callback hell but they introduce their own set of problems, which include:

  • Syntax seems more complicated than callbacks.
  • As the syntax is a little bit complicated, debugging can be more difficult.
  • The whole promise chain is asynchronous. Any function using a series of promises should return promise itself.

Async/Await in JavaScript

Introduced in ECMAScript back in 2017, Async/Await allows writing promises in an easier and more visually appealing way. It acts as syntactical sugar on top of promises. It was a great improvement used to handle asynchronous operations.

Note: Async functions make use of promises under the hood, so it’s important to first understand how promises work.

With Async/Await it is possible to write asynchronous, promise-based code just as if it is synchronous code without blocking the main thread. One of the other advantages of Async/Await is that we can get rid of then() chains.

Here is an example of how to use Async/Await in JavaScript:

async function verifyUserInfo() {
  try {
    const
      connection = await asyncDBconnect('http://localhost:1234'),
      session = await asyncGetSession(connection),
      user = await asyncGetUser(session),
      log = await asyncLogAccess(user);
    return log;
  }
  catch (error) {
    console.log('error', error);
    return null;
  }
}
// self-executing async function
(async () => { await verifyUserInfo(); })();
 

In this example, the function verifyUserInfo() is preceded by the async keyword.

Methods can be made asynchronous by writing async before their name; when such methods are called, they return a promise. As soon as the promise is returned, it should be either resolved or rejected.

The await keyword here is ensuring that the processing gets completed before the next command executes.

Benefits of Async/Await in JavaScript

Async/Await gives a better and clearer syntax and makes it easier to handle asynchronous operations. As promise comes with lots of boilerplate, Async/Await put a syntactical sugar over it. Let’s take a look at the benefits of Async/Await in JavaScript:

  • Cleaner syntax with fewer brackets, making debugging easier
  • Error handling is better; try..catch can be used in the same way it is used in synchronous code.
  • Easier to code

Read: Test Asynchronous Methods Using runs() and waitFor().

Asynchronous JavaScript

Asynchronous programming has always been a challenge in the programming world. In this blog, we talked about the evolution of asynchronous programming in JavaScript, from callbacks to promises to Async/Await.

JavaScript allows only one thing to happen at a time because it runs on a single thread. To make the processes faster, AJAX was introduced, which helped in evolving the upgrade of the web development process. Taking this into consideration, we have discussed some primary and advanced principles of JavaScript so that you can smartly handle AJAX and Asynchronous JavaScript in 2021.

 

Tariq Siddiqui
Tariq Siddiqui
A graduate in MS Computer Applications and a Web Developer from India with diverse skills across multiple Web development technologies. Enjoys writing about any tech topic, including programming, algorithms and cloud computing. Traveling and playing video games are the hobbies that interest me most.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured