DEV Community

Ayush Kumar Vishwakarma
Ayush Kumar Vishwakarma

Posted on

Understanding Asynchronous JavaScript: Callbacks, Promises, and Async/Await

Asynchronous programming is a fundamental concept in JavaScript, designed to handle tasks that take time to complete, like fetching data from an API, reading files, or executing timeouts. Unlike synchronous code that runs line by line, asynchronous code allows other operations to continue while waiting for a task to finish, making your applications more efficient and responsive. Let's dive into the three core approaches to handling asynchronous operations in JavaScript: callbacks, promises, and async/await.

  1. Callbacks A callback is a function passed into another function to be executed after the first function completes. Early JavaScript handled asynchronous tasks like events and timers exclusively through callbacks.

Example:

function fetchData(callback) {
  setTimeout(() => {
    callback("Data loaded");
  }, 2000);
}

fetchData(data => console.log(data));  // Prints "Data loaded" after 2 seconds
Enter fullscreen mode Exit fullscreen mode

However, callbacks can get messy when used in multiple layers, leading to "callback hell"—a situation where nested callbacks make code hard to read and maintain.

  1. Promises Promises were introduced to address issues with callbacks, especially error handling and readability. A promise is an object that represents a value that might not be available yet but will be resolved or rejected in the future.

A promise has three states:

Pending: The initial state, when the operation has not completed yet.
Fulfilled: The operation completed successfully.
Rejected: The operation failed.

Creating a Promise:

let fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data fetched");
  }, 2000);
});

fetchData
  .then(data => console.log(data))  // Runs if the promise is resolved
  .catch(error => console.log(error));  // Runs if the promise is rejected

Enter fullscreen mode Exit fullscreen mode

Promises make chaining asynchronous operations easier and help avoid nested code. For example, you can chain .then() methods to run multiple asynchronous tasks in order.

  1. Async/Await Async/await, introduced in ES2017, builds on promises and provides an even cleaner syntax for writing asynchronous code. An async function always returns a promise, and the await keyword pauses the function execution until the promise is resolved or rejected.

Example:

async function fetchData() {
  try {
    let data = await new Promise((resolve) => {
      setTimeout(() => resolve("Data fetched"), 2000);
    });
    console.log(data);  // Prints "Data fetched" after 2 seconds
  } catch (error) {
    console.error("Error:", error);
  }
}

fetchData();
Enter fullscreen mode Exit fullscreen mode

Async/await is often preferred because it reads more like synchronous code, making it easier to understand and maintain. Error handling is also more straightforward, using try...catch blocks around await calls.

Error Handling in Asynchronous Code
Error handling can vary depending on which approach you're using:

Callbacks: Typically use an error-first pattern where the first argument of the callback is an error (if one occurred).

Promises: Use .catch() to handle rejected promises.
Async/Await: Use try...catch to handle any errors in asynchronous functions.

Conclusion

Each of these methods—callbacks, promises, and async/await—provides a different approach to managing asynchronous operations in JavaScript.

Callbacks work but can lead to complex code.
Promises improve readability and allow chaining, making asynchronous operations easier to manage.
Async/Await brings simplicity and a synchronous style to asynchronous code, which is why it's often preferred in modern JavaScript.
Mastering these techniques is key to writing efficient, non-blocking JavaScript code, allowing your applications to run smoothly without unnecessary delays.

Top comments (1)

Collapse
 
kontactmaneesh profile image
kontactmaneesh

Nice