1. javascript
  2. /async

An Overview of Asynchronous JavaScript

Introduction

Asynchronous programming is a technique that allows a program to continue executing other tasks while waiting for a particular task to complete. This can be especially useful in JavaScript, where a single-threaded execution model can lead to a blocked execution and a frozen UI if a long-running task is not handled properly. With the advent of more and more web applications and services, Asynchronous programming has become a crucial aspect of web development.

Asynchronicity in Programming Languages

Asynchronicity in programming languages allows for more efficient and responsive applications, as the program can continue to work while waiting for a particular task to complete.

The concept of asynchronicity has been around for decades, with early examples in operating systems and concurrent programming languages such as Ada and Erlang. However, it has become more prevalent in recent years with the rise of web development and the need for responsive and efficient applications.

Modern-Day Usage

  • Managing constant streams of updates and notifications on social media platforms like Facebook and Twitter.

  • Loading and updating product pages, reviews, and shopping cart information in e-commerce sites like Amazon and eBay without the need to refresh the entire page.

  • Sending and receiving messages in real-time in chat and messaging apps like WhatsApp and Slack.

  • Managing updates and interactions in online games such as Fortnite and PUBG.

  • Loading and buffering video and audio content in streaming services like Netflix and Hulu.

  • Loading and updating articles and content in news and content sites like CNN and BBC.

  • Updating weather and news information in weather and news apps like AccuWeather and CNN.

  • Loading and updating map data in maps and navigation apps like Google Maps and Waze.

  • Handling high concurrency and scalability needs in back-end technologies such as Node.js.

  • Handling non-blocking I/O operations in databases like MongoDB.

The Prerequisites

Before diving into asynchronous JavaScript, it's advisable to have a solid understanding of:

  • JavaScript's execution model, which is single-threaded.

  • JavaScript's event loop and how it manages the execution of tasks.

  • The Document Object Model (DOM) and how it handles the manipulation of HTML and XML documents.

Synchronicity and Synchronous Functions

Synchronous code, also known as blocking code, is executed one line at a time in the order it is written. This means that the program will wait for a synchronous function to complete before moving on to the next line of code. For example, the following synchronous code will log "Fetching Data" and then "Data fetched" to the console:

console.log("Fetching Data...");
let data = fetchData();
console.log("Data fetched: " + data);

The program will wait for the fetchData() function to complete before moving on to the next line of code. Potentially, this can cause problems when the fetchData() function takes a long time to execute, as it will block the execution of any other functions that are waiting to be called.

The Call Stack

The call stack is a data structure that keeps track of the execution of a program. It is a Last In, First Out (LIFO) data structure where the most recent function call is placed on top of the stack and the first function call is at the bottom. Each time a function is called, it is pushed onto the call stack and when the function returns, it is popped off the call stack.

In JavaScript, the call stack is single-threaded, which means that only one function can be executed at a time. This can cause problems when a function takes a long time to execute, as it will block the execution of any other functions that are waiting to be called, leading to potential errors.

To illustrate this, let's see how the call stack works in a synchronous execution:

console.log("Start");
function first(){
    console.log("Inside first function");
    second();
    console.log("Exiting first function");
}

function second(){
    console.log("Inside second function");
}

first();
console.log("End");

The call stack execution will look like this:

first()
second()

As we can see, the first function calls the second function, and the second function is pushed on top of the call stack, after the second function finishes it pops off, and the first function continues its execution. The final output will be:

"Start"
"Inside first function"
"Inside second function"
"Exiting first function"
"End"

Understanding the call stack will help us move on to handling asynchronous code using callbacks and promises as we'll see in the next sections.

Understanding Asynchronous JavaScript

JavaScript achieves asynchronicity through the use of callback functions, promises, and other mechanisms such as the event loop and the web API.

One of the main benefits of asynchronous programming is the ability to handle long-running tasks without freezing the UI. For example, when fetching data from a server, the program can continue executing other tasks while it waits for the data to be returned. In turn, we get a more responsive and efficient application, as the user can continue to interact with the application while it is waiting for data.

Another benefit of asynchronous programming is the ability to handle multiple tasks at the same time, also known as concurrency. This is particularly useful in web development, where a single-threaded execution model can lead to a blocked execution if a long-running task is not handled properly.

A simple example of asynchronous code in JavaScript is the setTimeout() function, which allows you to execute a function after a certain amount of time.

setTimeout(() => {
    console.log("Executed after 2 seconds");
}, 2000);
console.log("Executed immediately");

As you can see, the setTimeout() function is called asynchronously. It doesn't block the execution of the rest of the code, and the second console.log is executed immediately.

Promises as an Alternative to Callbacks in ES6

Asynchronous JavaScript has evolved a lot, and ES6 introduced a new way of handling asynchronicity through the use of promises. A promise is an object that represents the eventual completion of an asynchronous operation. Promises can be in one of three states: pending, fulfilled, or rejected. A promise can be used to handle callbacks in a more readable and maintainable way.

For example, the following code fetches some data from a server and performs some calculations on that data using a promise:

let promise = fetchData();
promise.then((data) => {
  console.log("Data fetched: " + data);
  let result = performCalculations(data);
  console.log("Calculations completed: " + result);
});
console.log("Fetching Data...");

The fetchData() function returns a promise that is pending. The promise is then fulfilled when the data is fetched, and the then() method is called, allowing the program to perform the calculations on the data.

Moreover, promises provide a more readable and maintainable way to handle callbacks and errors, as they allow for a consistent way to handle both success and failure cases. Additionally, they can be easily chained together, making it easier to write complex asynchronous logic.

They also have a catch() method that allows you to handle errors and a finally() method that allows you to run code after the promise has been resolved or rejected.

let promise = fetchData();
promise
  .then((data) => {
    console.log("Data fetched: " + data);
    let result = performCalculations(data);
    console.log("Calculations completed: " + result);
  })
  .catch((error) => {
    console.error("Error: " + error);
  })
  .finally(() => {
    console.log("Promise finished");
  });
console.log("Fetching Data...");
  • The then() method:
    • logs the fetched data
    • performs calculations with it
  • The catch() method:
    • logs any errors
  • The finally() method:
    • logs that the promise is finished
  • The last console.log statement is called immediately after the promise is created
  • The fetching of data happens in the background.

Conclusion

  • Asynchronous programming allows a program to continue executing other tasks while waiting for a particular task to complete.

  • JavaScript uses callback functions and promises to achieve asynchronicity.

  • Understanding JavaScript's single-threaded execution model, event loop, and the DOM is important for understanding asynchronous JavaScript.

  • Callbacks and promises provide an alternative way of handling asynchronicity that is more readable and maintainable.

  • Asynchronous programming has become prevalent in web development due to the need for responsive and efficient applications.

  • Understanding the Call stack and the Event loop is crucial for debugging and understanding complex asynchronous code.