How to Handle Asynchronous JavaScript: Promises and Async/Await

Table of Contents

  1. Understanding Asynchronous JavaScript
  2. Promises
    • What are Promises?
    • Creating and Using Promises
    • Chaining Promises
  3. Async/Await
    • What is Async/Await?
    • Using Async/Await
    • Error Handling with Async/Await
  4. Common Practices and Best Practices
  5. Conclusion
  6. References

1. Understanding Asynchronous JavaScript

Asynchronous operations in JavaScript do not block the execution of other code. Instead of waiting for an operation to finish, JavaScript moves on to the next line of code and comes back to handle the result of the asynchronous operation later.

For example, consider the setTimeout function:

console.log('Before setTimeout');
setTimeout(() => {
    console.log('Inside setTimeout');
}, 2000);
console.log('After setTimeout');

In this code, the setTimeout function schedules a callback function to be executed after 2 seconds. JavaScript does not wait for the 2 seconds to pass; it immediately moves on to the next line and prints “After setTimeout”. After 2 seconds, the callback function inside setTimeout is executed, and “Inside setTimeout” is printed.

2. Promises

What are Promises?

A Promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. A Promise can be in one of three states:

  • Pending: The initial state; the promise is neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

Creating and Using Promises

You can create a Promise using the Promise constructor, which takes a function with two parameters: resolve and reject. The resolve function is used to fulfill the promise, and the reject function is used to reject it.

const myPromise = new Promise((resolve, reject) => {
    // Simulating an asynchronous operation
    setTimeout(() => {
        const randomNumber = Math.random();
        if (randomNumber < 0.5) {
            resolve(randomNumber);
        } else {
            reject(new Error('Random number is greater than or equal to 0.5'));
        }
    }, 1000);
});

myPromise
  .then((result) => {
        console.log('Promise fulfilled with result:', result);
    })
  .catch((error) => {
        console.log('Promise rejected with error:', error.message);
    });

In this example, we create a Promise that simulates an asynchronous operation using setTimeout. After 1 second, it generates a random number. If the random number is less than 0.5, the promise is fulfilled with the random number; otherwise, it is rejected with an error.

The then method is used to handle the fulfilled state of the promise, and the catch method is used to handle the rejected state.

Chaining Promises

Promises can be chained together to perform a sequence of asynchronous operations. Each then method returns a new promise, which can be used to chain another then or catch method.

function asyncOperation1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Result of operation 1');
        }, 1000);
    });
}

function asyncOperation2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(result1 + ' -> Result of operation 2');
        }, 1000);
    });
}

asyncOperation1()
  .then((result1) => {
        console.log(result1);
        return asyncOperation2(result1);
    })
  .then((result2) => {
        console.log(result2);
    })
  .catch((error) => {
        console.error(error);
    });

In this example, we have two asynchronous operations represented by functions asyncOperation1 and asyncOperation2. We chain these operations together using then methods. The result of the first operation is passed as an argument to the second operation.

3. Async/Await

What is Async/Await?

Async/Await is a syntactic sugar built on top of Promises. It allows you to write asynchronous code in a more synchronous - looking way. An async function always returns a Promise. Inside an async function, you can use the await keyword to pause the execution of the function until a Promise is fulfilled or rejected.

Using Async/Await

function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('Fetched data');
        }, 1000);
    });
}

async function main() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error(error);
    }
}

main();

In this example, we have a fetchData function that returns a Promise. The main function is an async function. Inside the main function, we use the await keyword to wait for the fetchData Promise to be fulfilled. The try - catch block is used to handle any errors that may occur.

Error Handling with Async/Await

As shown in the previous example, you can use a try - catch block to handle errors in an async function. If a Promise is rejected, the control jumps to the catch block.

function asyncOperation() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('Operation failed'));
        }, 1000);
    });
}

async function handleError() {
    try {
        await asyncOperation();
    } catch (error) {
        console.log('Error caught:', error.message);
    }
}

handleError();

4. Common Practices and Best Practices

Common Practices

  • Use Promises for all asynchronous operations: Promises provide a consistent way to handle asynchronous operations and make it easier to manage the flow of your code.
  • Chain Promises for sequential operations: When you need to perform a series of asynchronous operations one after another, chain the Promises using then methods.
  • Use Async/Await for readability: Async/Await can make your asynchronous code more readable, especially when dealing with complex asynchronous operations.

Best Practices

  • Error handling: Always handle errors in Promises using catch methods or try - catch blocks in async functions. This helps prevent unhandled rejections, which can lead to hard - to - debug issues.
  • Avoid callback hell: Promises and Async/Await are designed to avoid the nested callback structure known as “callback hell”. Use them to keep your code more organized.
  • Keep Promises short and focused: Each Promise should represent a single, well - defined asynchronous operation. This makes the code easier to understand and maintain.

Conclusion

Handling asynchronous JavaScript is an essential skill for any JavaScript developer. Promises and Async/Await provide powerful and elegant ways to deal with asynchronous operations. Promises offer a structured approach to managing the state of asynchronous operations, while Async/Await builds on Promises to make asynchronous code look more like synchronous code, improving readability.

By understanding the concepts, usage methods, and best practices of Promises and Async/Await, you can write more efficient, maintainable, and responsive JavaScript applications.

References