Instagram
youtube
Facebook
Twitter

Promises and Async/Await

In this lesson, we will delve into Promises and Async/Await, two essential concepts in JavaScript that allow for handling asynchronous operations in a more manageable and readable way. Asynchronous programming is crucial in JavaScript, especially for operations like fetching data from APIs, reading files, or performing time-consuming tasks without blocking the execution of other code.


1. Understanding Asynchronous Programming

Asynchronous programming allows a program to perform other tasks while waiting for an operation to complete. This is particularly important in JavaScript, which is single-threaded, meaning it can only execute one task at a time. By using asynchronous programming, we can avoid blocking the main thread and ensure a smoother user experience.

2. What are Promises?

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It is a way to handle asynchronous operations in a more manageable manner than using traditional callback functions.

2.1 Promise States

A Promise can be in one of three states:

  1. Pending: The initial state; the operation is not yet completed.
  2. Fulfilled: The operation completed successfully.
  3. Rejected: The operation failed.

2.2 Creating a Promise

You can create a Promise using the Promise constructor, which takes a function (known as the executor) that has two parameters: resolve and reject.

Example

let myPromise = new Promise((resolve, reject) => {
    let success = true; // Change this to false to simulate a failure

    if (success) {
        resolve("Operation was successful!");
    } else {
        reject("Operation failed!");
    }
});

2.3 Consuming a Promise

To handle the result of a Promise, you use the .then() method for successful resolutions and the .catch() method for rejections.

Example

myPromise
    .then(result => {
        console.log(result); // Outputs: Operation was successful!
    })
    .catch(error => {
        console.error(error); // Outputs: Operation failed!
    });

2.4 Chaining Promises

You can chain multiple .then() methods to perform a sequence of asynchronous operations.

Example

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

fetchData
    .then(result => {
        console.log(result); // Outputs: Data fetched!
        return "Processing data...";
    })
    .then(result => {
        console.log(result); // Outputs: Processing data...
    });

3. Error Handling in Promises

Handling errors in Promises is crucial for maintaining the robustness of your code. You can handle errors in two ways:

  1. Using .catch() at the end of a Promise chain.
  2. Using a try/catch block inside an async function when using async/await.

Example

let fetchDataWithError = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject("Failed to fetch data!");
    }, 2000);
});

fetchDataWithError
    .then(result => {
        console.log(result);
    })
    .catch(error => {
        console.error(error); // Outputs: Failed to fetch data!
    });

4. Introduction to Async/Await

Async/Await is syntactic sugar built on top of Promises that allows you to write asynchronous code in a more synchronous manner.

4.1 The async Keyword

The async keyword is placed before a function declaration to define an asynchronous function. This function always returns a Promise.

Example

async function fetchData() {
    return "Data fetched!";
}

fetchData().then(result => {
    console.log(result); // Outputs: Data fetched!
});

4.2 The await Keyword

The await keyword can only be used inside an async function. It pauses the execution of the async function until the Promise is resolved or rejected, making it easier to read and write asynchronous code.

Example

async function fetchData() {
    let data = await new Promise((resolve) => {
        setTimeout(() => {
            resolve("Data fetched!");
        }, 2000);
    });

    console.log(data); // Outputs: Data fetched!
}

fetchData();

5. Error Handling with Async/Await

Error handling can be done using a try/catch block within an async function.

Example

async function fetchDataWithError() {
    try {
        let data = await new Promise((resolve, reject) => {
            setTimeout(() => {
                reject("Failed to fetch data!");
            }, 2000);
        });
        console.log(data);
    } catch (error) {
        console.error(error); // Outputs: Failed to fetch data!
    }
}

fetchDataWithError();

6. Practical Examples

6.1 Fetching Data from an API

Using async/await, you can easily fetch data from an API.

Example

async function getUserData(userId) {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        let userData = await response.json();
        console.log(userData);
    } catch (error) {
        console.error("Error fetching user data:", error);
    }
}

getUserData(1);

6.2 Sequential and Parallel Requests

You can make sequential and parallel requests using async/await.

Sequential Requests:

async function fetchSequentialData() {
    let user1 = await fetchUser(1);
    let user2 = await fetchUser(2);
    console.log(user1, user2);
}

fetchSequentialData();

Parallel Requests:

async function fetchParallelData() {
    let user1Promise = fetchUser(1);
    let user2Promise = fetchUser(2);

    let [user1, user2] = await Promise.all([user1Promise, user2Promise]);
    console.log(user1, user2);
}

fetchParallelData();

7. Coding Exercises

Create a JavaScript file named promisesAndAsync.js and complete the following exercises:

  1. Create a Promise: Write a Promise that resolves after 2 seconds with a message "Resolved!".
  2. Fetch User Data: Write an async function that fetches user data from the JSONPlaceholder API and logs the user's name.
  3. Error Handling: Modify the above function to handle errors gracefully.

Example Solutions:

// 1. Create a Promise
let myPromise = new Promise((resolve) => {
    setTimeout(() => {
        resolve("Resolved!");
    }, 2000);
});

myPromise.then(message => {
    console.log(message); // Outputs: Resolved!
});

// 2. Fetch User Data
async function fetchUserData(userId) {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        let userData = await response.json();
        console.log("User Name:", userData.name);
    } catch (error) {
        console.error("Error fetching user data:", error);
    }
}

fetchUserData(1); // Outputs: User Name: Leanne Graham

// 3. Error Handling
async function fetchUserDataWithErrorHandling(userId) {
    try {
        let response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        if (!response.ok) throw new Error("Network response was not ok");
        let userData = await response.json();
        console.log("User Name:", userData.name);
    } catch (error) {
        console.error("Error fetching user data:", error);
    }
}

fetchUserDataWithErrorHandling(100); // Outputs: Error fetching user data: ...

Conclusion

In this lesson, we learned about Promises and the Async/Await syntax in JavaScript. These concepts are essential for handling asynchronous operations effectively, allowing for cleaner and more readable code. Understanding how to use Promises and Async/Await will significantly enhance your ability to work with asynchronous programming in JavaScript.

In the next lesson, we will cover Error Handling, where you'll learn how to manage exceptions and errors more effectively in your JavaScript applications.