DEV Community

Cover image for Understanding Asynchronous JavaScript: From Callbacks to Async/Await
Abhijeet Chaudhari
Abhijeet Chaudhari

Posted on

Understanding Asynchronous JavaScript: From Callbacks to Async/Await

Understanding Callbacks, Promises, Async/Await

JavaScript operates on a single-threaded model, handling one task at a time. Yet, modern web applications often require handling multiple operations concurrently, such as data fetching, file operations, or user interactions. To address this, JavaScript employs asynchronous programming techniques. This post delves into three fundamental asynchronous JavaScript concepts: callbacks, promises, and async/await.

Callbacks: The Traditional Method

Callbacks are functions passed as arguments to other functions and executed upon completion of an operation. They are a basic approach to managing asynchronous tasks in JavaScript.

Example of Callbacks

function retrieveData(callback) {
    setTimeout(() => {
        console.log("Successfully retrieved data from the server");
        callback();
    }, 2000);
}

function handleData() {
    console.log("Data processing initiated...");
}

retrieveData(handleData);

Enter fullscreen mode Exit fullscreen mode

In this example, retrieve Data simulates data retrieval with a delay. Once the data is ready, it invokes the handleData function.

Challenges with Callbacks

While callbacks are simple, they can lead to "callback hell," where multiple nested callbacks make the code complex and difficult to manage.

function retrieveData(callback) {
    setTimeout(() => {
        console.log("Data retrieved from the server");
        callback();
    }, 2000);
}

function handleData(callback) {
    console.log("Processing data...");
    callback();
}

function showData() {
    console.log("Data displayed...");
}

retrieveData(() => {
    handleData(() => {
        showData();
    });
});

Enter fullscreen mode Exit fullscreen mode

Promises: A More Structured Approach

Promises provide a more organized way to handle asynchronous operations. A promise represents a value that may be available immediately, later, or never, and can be in one of three states: pending, fulfilled, or rejected.

Pending: **The initial state of a promise; neither fulfilled nor rejected.
**Fulfilled:
The state of a promise representing a successful operation.
Rejected: The state of a promise representing a failed operation.

Example of Promises

function retrieveData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data retrieved from the server");
        }, 2000);
    });
}

retrieveData()
    .then((message) => {
        console.log(message);
        return "Data processing initiated...";
    })
    .then((message) => {
        console.log(message);
        return "Data displayed...";
    })
    .then((message) => {
        console.log(message);
    })
    .catch((error) => {
        console.error("An error occurred:", error);
    });

Enter fullscreen mode Exit fullscreen mode

In this example, retrieveData returns a promise. The .then method handles the resolved value, allowing for chaining of asynchronous operations in a clear manner.

Advantages of Promises

  • Chaining: Promises can be chained, making the code more readable.
  • Error Handling: Promises use the catch method for error handling, which simplifies managing errors.

Async/Await: Simplifying Asynchronous Code

Async/await, introduced in ES2017, allows writing asynchronous code in a synchronous style. It is built on promises and enhances code readability.

Example of Async/Await

function retrieveData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data retrieved from the server");
        }, 2000);
    });
}

async function manageData() {
    try {
        const data = await retrieveData();
        console.log(data);
        console.log("Data processing initiated...");
        console.log("Data displayed...");
    } catch (error) {
        console.error("An error occurred:", error);
    }
}

manageData();

Enter fullscreen mode Exit fullscreen mode

In this example, retrieveData returns a promise, and manageData uses the async keyword. The await keyword pauses execution until the promise is resolved.

Benefits of Async/Await

Readability: Async/await makes asynchronous code appear synchronous, improving readability.
Error Handling: It uses try/catch blocks, which are familiar to those experienced with synchronous code.

More About Promises - Types and features

Handling Multiple Promises

When dealing with multiple asynchronous operations, JavaScript provides several methods to manage them collectively. These methods are particularly useful when you need to coordinate multiple promises.

Promise.all

Promise.all takes an iterable of promises and returns a single promise that resolves when all of the input promises have resolved, or rejects if any of the input promises reject. This method is useful when you need all operations to complete successfully.

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // Output: [3, 42, "foo"]
});

Enter fullscreen mode Exit fullscreen mode

In this example, Promise.all waits for all promises to resolve and then returns an array of their results

Promise.race

Promise.race returns a promise that settles as soon as the first promise in the iterable settles (either resolves or rejects). This can be useful for setting timeouts or when you need the first response from multiple sources.

const promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'one'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'two'));

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // Output: "two"
});

Enter fullscreen mode Exit fullscreen mode

Here, promise2 resolves faster than promise1, so Promise.race returns the result of promise2.

Promise.allSettled

Promise.allSettled returns a promise that resolves after all the given promises have either resolved or rejected, with an array of objects that each describes the outcome of each promise. This is useful when you need to know the result of each promise regardless of whether it resolved or rejected.

const promise1 = Promise.resolve(3);
const promise2 = new Promise((_, reject) => setTimeout(reject, 100, 'Error'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 200, 'foo'));

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  results.forEach((result) => console.log(result.status));
  // Output:
  // "fulfilled"
  // "rejected"
  // "fulfilled"
});

Enter fullscreen mode Exit fullscreen mode

In this example, Promise.allSettled waits for all promises to settle and returns an array of objects with the status and value (or reason for rejection) of each promise.

Conclusion

Asynchronous programming is crucial in JavaScript, enabling efficient handling of multiple tasks without blocking the main thread. Callbacks, while straightforward, can lead to complex code. Promises offer a cleaner approach with better readability and error handling. Async/await builds on promises, providing an even more intuitive syntax for writing asynchronous code. Mastering these concepts will help you write more efficient and maintainable JavaScript applications.

Top comments (0)