Table of contents
1.
Introduction
2.
What is Synchronous Code?
3.
What is Asynchronous Code?
4.
How Callbacks Work in JavaScript
5.
Using Callbacks
6.
Continuing execution...
6.1.
Syntax
7.
How Promises Work in JavaScript
7.1.
Syntax
8.
How to Consume Promises
8.1.
1. Using `.then()` & `.catch()`
8.2.
2. Using `async/await`
9.
Frequently Asked Questions
9.1.
What is the main difference between synchronous & asynchronous code execution in JavaScript?
9.2.
What are callbacks in JavaScript & how are they used with asynchronous operations?
9.3.
What are the benefits of using promises over callbacks in JavaScript?
10.
Conclusion
Last Updated: Oct 18, 2024
Easy

Asynchronous Javascript

Author Rinki Deka
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

JavaScript is a programming language which is used to create interactive web pages & applications. It allows developers to add dynamic behavior to websites, which makes them more engaging & responsive. One important concept in JavaScript is asynchronous programming, which enables the execution of code without blocking the main thread. This means that instead of waiting for a long-running task to complete before moving on, JavaScript can continue running other code in the meantime. 

Asynchronous Javascript

In this article, we'll see what asynchronous JavaScript is, how it differs from synchronous code, & the different ways to work with asynchronous operations using callbacks & promises. 

What is Synchronous Code?

In programming, synchronous code refers to a sequence of instructions that execute one after the other in a specific order. Each line of code waits for the previous line to finish before starting its own execution. This means that if a particular operation takes a long time to complete, it will block the execution of the rest of the code until it finishes.

For example : 

console.log("Start");
console.log("Processing...");
for (let i = 0; i < 5000000000; i++) {
  // Simulate a long-running operation
}
console.log("End");


In this code snippet, the `console.log` statements will execute in the order they appear. The loop in the middle simulates a long-running operation. Until that loop completes, the final `console.log("End")` statement will not execute. This is because each line of code waits for the previous line to finish before moving on.

Synchronous code is easy to understand, but it can lead to performance issues if there are time-consuming operations involved. If a web page relies heavily on synchronous code, it may appear unresponsive or frozen until all the operations are completed, which hampers the user experience.

What is Asynchronous Code?

Asynchronous code, in contrast to synchronous code, allows multiple tasks to be executed simultaneously without blocking the execution of other parts of the program. This means that when an asynchronous operation is initiated, the code continues running without waiting for that operation to complete. Once the asynchronous task finishes, it notifies the program of its completion.

Asynchronous programming is useful when you are dealing with time-consuming operations like making HTTP requests, reading large files, or querying a database. With asynchronous code, the program can continue responding to user interactions or perform other tasks while waiting for these operations to finish.

For example :

console.log("Start");
setTimeout(() => {
  console.log("Async operation completed");
}, 2000);
console.log("End");
You can also try this code with Online Javascript Compiler
Run Code


In this example, we use the `setTimeout` function to simulate an asynchronous operation. The `setTimeout` function takes a callback function as the first argument & a delay (in milliseconds) as the second argument. The callback function will be executed after the specified delay.

The output of this code will be:

Start
End
Async operation completed


As you can see, the code continues executing & logs "End" to the console before the asynchronous operation completes. After the 2-second delay, the callback function is executed, logging "Async operation completed" to the console.

Asynchronous programming allows for better performance & responsiveness in applications, especially when dealing with time-consuming tasks.

How Callbacks Work in JavaScript

Callbacks are a fundamental concept in JavaScript & are commonly used to handle asynchronous operations. A callback is a function that is passed as an argument to another function & is expected to be executed at a later time, usually when an asynchronous operation has been completed.

Let’s see an example of how callbacks work:

function fetchData(url, callback) {
  // Simulating an asynchronous operation
  setTimeout(() => {
    const data = { id: 1, name: "John Doe" };
    callback(data);
  }, 2000);
}
function processData(data) {
  console.log("Processing data:", data);
}
fetchData("https://api.example.com/data", processData);
console.log("Continuing execution...");
You can also try this code with Online Javascript Compiler
Run Code


In this example, the `fetchData` function simulates an asynchronous operation, like making an API request. It takes a `url` parameter & a `callback` function as arguments. Inside `fetchData`, we use `setTimeout` to simulate a delay of 2 seconds before invoking the callback function with the fetched data.

The `processData` function is defined to handle the data once it's available. It simply logs the received data to the console.

When we call `fetchData` & pass the `processData` function as the callback, the execution continues immediately to the next line, logging "Continuing execution..." to the console. After the 2-second delay, the callback function `processData` is invoked with the fetched data.


Output:

Continuing execution...
Processing data: { id: 1, name: "Rahul Singh" }


Callbacks allow us to structure our code in a way that handles asynchronous operations without blocking the execution flow. However, when you are dealing with multiple asynchronous operations that depend on each other, callbacks can lead to deeply nested code, which is often referred to as "callback hell."

Using Callbacks

Callbacks are widely used in JavaScript to handle asynchronous operations. Let's look at a more practical example of using callbacks to read a file asynchronously using the built-in `fs` (File System) module in Node.js.

const fs = require('fs');
function readFileAsync(filePath, callback) {
  fs.readFile(filePath, 'utf8', (err, data) => {
    if (err) {
      callback(err);
    } else {
      callback(null, data);
    }
  });
}

readFileAsync('example.txt', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
  } else {
    console.log('File contents:', data);
  }
});
console.log('Continuing execution...');

 

  • In this example, we define a `readFileAsync` function that takes a `filePath` & a `callback` function as arguments. Inside the function, we use the `fs.readFile` method to read the contents of the file asynchronously. The `fs.readFile` method itself takes a callback function that is invoked when the file reading operation is complete.
     
  • If an error occurs during the file reading process, we invoke the callback function with the error object. Otherwise, we invoke the callback with `null` as the first argument & the file contents as the second argument.
     
  • We then call the `readFileAsync` function, passing the file path & a callback function that handles the result. If an error occurs, we log the error to the console. If the file is successfully read, we log the file contents.

Continuing execution...

File contents: This is the content of the example file.

 

As you can see, the code continues executing & logs "Continuing execution..." before the file reading operation completes. Once the file is read, the callback function is invoked, either with an error or with the file contents.
 

Note: Taking the help of callbacks allows us to handle asynchronous operations in a non-blocking manner, enabling our code to continue executing while waiting for the asynchronous task to complete.

Syntax


When you are working with callbacks in JavaScript, it's important to understand the syntax & how to define & use callback functions. The syntax for using callbacks is :

function asyncOperation(param1, param2, callback) {
  // Perform the asynchronous operation
  // ...
  // Invoke the callback function with the result or error
  callback(error, result);
}

asyncOperation(arg1, arg2, (error, result) => {
  if (error) {
    // Handle the error
  } else {
    // Process the result
  }
});


In this syntax : 

1. The `asyncOperation` function takes two parameters (`param1` & `param2`) & a `callback` function as arguments.
 

2. Inside the `asyncOperation` function, the asynchronous operation is performed.
 

3. Once the asynchronous operation is complete, the `callback` function is invoked with two arguments: `error` & `result`.
 

   - If an error occurs during the asynchronous operation, the `error` argument is populated with the error object, & the `result` argument is typically `null` or `undefined`.
 

   - If the asynchronous operation is successful, the `error` argument is `null`, & the `result` argument contains the result of the operation.
 

4. When calling the `asyncOperation` function, we provide the necessary arguments (`arg1` & `arg2`) & a callback function.
 

5. The callback function takes two parameters: `error` & `result`.
 

6. Inside the callback function, we first check if an error occurred by evaluating the `error` argument.
 

   - If an error is present, we handle it accordingly (e.g., log the error, throw an exception, or perform error-specific actions).

   - If no error occurred (`error` is `null`), we process the `result` as needed.


It's important to note that the names `error` & `result` are conventions commonly used in Node.js & JavaScript, but you can choose any valid parameter names for the callback function.

How Promises Work in JavaScript

Promises were introduced in JavaScript as a way to handle asynchronous operations more elegantly & avoid the problem of callback hell. A promise represents the eventual completion or failure of an asynchronous operation & allows you to chain operations together.

A promise can be in one of three states:

1. Pending: The initial state when the promise is created & the asynchronous operation has not yet been completed.
 

2. Fulfilled: The state when the asynchronous operation has successfully completed, & the promise holds a resolved value.
 

3. Rejected: The state when the asynchronous operation has encountered an error or failed to complete, & the promise holds a reason for the failure.


Let’s look at an example of creating & using a promise:

function fetchData(url) {
  return new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
      const data = { id: 1, name: "John Doe" };
      resolve(data);
    }, 2000);
  });
}
fetchData("https://api.example.com/data")
  .then((data) => {
    console.log("Data:", data);
  })
  .catch((error) => {
    console.error("Error:", error);
  });
You can also try this code with Online Javascript Compiler
Run Code


Output

Data: { id: 1, name: 'John Doe' }


In this example, the `fetchData` function returns a new promise. Inside the promise constructor, we simulate an asynchronous operation using `setTimeout`. If the operation is successful, we call the `resolve` function with the data. If an error occurs, we would call the `reject` function with the error reason.

To use the promise, we chain the `.then()` method to handle the fulfilled state of the promise. Inside the `.then()` callback, we receive the resolved data & can perform further operations with it. We can also chain the `.catch()` method to handle any errors that may occur during the promise execution.

Note: Promises provide a cleaner & more readable way to handle asynchronous operations compared to callbacks. They allow you to chain multiple asynchronous operations together using `.then()` & handle errors centrally using `.catch()`.

Syntax

The syntax for creating & using promises in JavaScript is : 

const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  // ...
  if (/* operation successful */) {
    resolve(result);
  } else {
    reject(error);
  }
});


promise.then((result) => {
  // Handle the fulfilled state
}).catch((error) => {
  // Handle the rejected state
});


In this syntax : 

1. A new promise is created using the `new Promise()` constructor, which takes a function (called the executor) as an argument.
 

2. The executor function receives two arguments: `resolve` & `reject`, which are functions used to control the state of the promise.
 

3. Inside the executor function, you perform the asynchronous operation.
 

4. If the asynchronous operation is successful, you call the `resolve` function with the result.
 

5. If an error occurs during the asynchronous operation, you call the `reject` function with the error reason.
 

6. The promise object returned by the constructor has two important methods: `.then()` & `.catch()`.
 

7. The `.then()` method is used to handle the fulfilled state of the promise. It takes a callback function as an argument, which receives the resolved value.
 

8. The `.catch()` method is used to handle the rejected state of the promise. It takes a callback function as an argument, which receives the error reason.
 

9. You can chain multiple `.then()` methods to perform sequential asynchronous operations, where the return value of one `.then()` becomes the input to the next `.then()`.
 

10. The `.catch()` method can be used at the end of the promise chain to handle any errors that occur during the execution of the promise.

How to Consume Promises

Consuming promises refers to the process of using the values returned by promises once they are fulfilled or handling any errors if they are rejected. Here are a few common ways to consume promises:

1. Using `.then()` & `.catch()`

   fetchData("https://api.example.com/data")
     .then((data) => {
       console.log("Data:", data);
       // Perform further operations with the data
     })
     .catch((error) => {
       console.error("Error:", error);
       // Handle the error
     });


In this example, we use `.then()` to handle the fulfilled state of the promise & receive the resolved data. We can perform any necessary operations with the data inside the `.then()` callback. The `.catch()` method is used to handle any errors that may occur during the promise execution.

2. Using `async/await`

   async function fetchDataAsync() {
     try {
       const data = await fetchData("https://api.example.com/data");
       console.log("Data:", data);
       // Perform further operations with the data
     } catch (error) {
       console.error("Error:", error);
       // Handle the error
     }
   }

   fetchDataAsync();


`async/await` is a syntax that allows you to write asynchronous code that looks more like synchronous code. By using the `await` keyword before a promise, the code execution waits until the promise is resolved or rejected. The resolved value is then assigned to the variable (`data` in this example). Any errors thrown during the promise execution can be caught using a `try/catch` block.

Note: Consuming promises effectively allows you to handle asynchronous operations & work with their results in a structured & readable manner.

Frequently Asked Questions

What is the main difference between synchronous & asynchronous code execution in JavaScript?

Synchronous code executes line by line, blocking the execution until each operation completes. Asynchronous code, on the other hand, allows multiple operations to be initiated without blocking, & the program continues executing while waiting for the asynchronous tasks to complete.

What are callbacks in JavaScript & how are they used with asynchronous operations?

Callbacks are functions passed as arguments to other functions & are invoked when an asynchronous operation completes. They allow you to specify what should happen after the asynchronous task finishes, whether it's handling the result or an error.

What are the benefits of using promises over callbacks in JavaScript?

Promises provide a cleaner & more readable syntax compared to callbacks, avoiding the problem of callback hell. They allow you to chain multiple asynchronous operations together using `.then()` & handle errors centrally using `.catch()`. Promises also provide better error handling & make it easier to reason about the flow of asynchronous code.

Conclusion

In this article, we explained the concept of asynchronous JavaScript & how it differs from synchronous code execution. We learned about callbacks, promises, & the syntax for working with them. Callbacks are functions passed as arguments to handle the results of asynchronous operations, while promises provide a more structured & readable approach to handling asynchronous tasks. 

You can also check out our other blogs on Code360.

Live masterclass