Published on

Part 8: Asynchronous JavaScript

Authors

Welcome back to the JavaScript Crash Course series! 🎉 In Part 7, we covered advanced event handling, learning techniques like event delegation, throttling, and debouncing. Today, in Part 8, we’ll explore asynchronous JavaScript—an essential skill for handling tasks that take time, like fetching data from a server or waiting for user input. Understanding asynchronous code will open up new possibilities in web development. Ready? Let’s dive in! 🚀

What is Asynchronous JavaScript? 🤔

JavaScript is single-threaded, meaning it can only do one thing at a time. However, some tasks, like fetching data from an API, can take time. Asynchronous JavaScript allows us to handle these tasks without freezing or slowing down the page.

When a task is asynchronous, JavaScript can "pause" it and continue running other code, then return to the task once it’s ready.


Callbacks 📞

Callbacks are one of the earliest ways to handle asynchronous tasks in JavaScript. A callback is a function passed into another function, to be executed after the first function completes.

Example: Using Callbacks

Here’s a simple example simulating an API request with a setTimeout() to represent a delay.

function fetchData(callback) {
    setTimeout(() => {
        console.log("Data fetched!");
        callback("Here is your data.");
    }, 2000);
}

fetchData((data) => {
    console.log(data);
});

In this example, fetchData waits 2 seconds before logging "Data fetched!" and then executes the callback with the data.

Drawbacks of Callbacks

Callbacks work, but they can lead to callback hell if you have multiple asynchronous tasks, as each callback gets nested within the previous one.


Promises 📝

Promises are a newer way to handle asynchronous tasks, introduced to make async code easier to read and manage. A promise is an object representing the eventual completion (or failure) of an asynchronous operation.

Creating and Using Promises

Here’s how to create and use a promise:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let success = true; // Simulate success or failure
            if (success) {
                resolve("Data fetched successfully!");
            } else {
                reject("Failed to fetch data.");
            }
        }, 2000);
    });
}

fetchData()
    .then((data) => {
        console.log(data); // Output if resolved
    })
    .catch((error) => {
        console.error(error); // Output if rejected
    });

In this example, fetchData returns a promise. If successful, it resolves with a success message; if not, it rejects with an error. We handle both cases with .then() and .catch().


Async/Await ⏳

Async/await is built on top of promises and offers a cleaner, more readable way to work with asynchronous code. Using async and await allows us to write asynchronous code that looks more like synchronous code.

Example: Using Async/Await

To use await, the function must be declared with async. Here’s how:

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

async function getData() {
    try {
        let data = await fetchData();
        console.log(data); // Output: "Data fetched successfully!"
    } catch (error) {
        console.error("Error:", error);
    }
}

getData();

In this example, await fetchData() pauses the getData function until fetchData completes, making the code easier to follow than chaining .then().


Practical Example: Fetching Data from an API 🌐

Let’s use what we’ve learned to fetch data from a public API. We’ll use the JSONPlaceholder API, which simulates typical API responses.

Example: Fetching API Data with Async/Await

async function fetchPosts() {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/posts");
        let posts = await response.json();
        console.log(posts);
    } catch (error) {
        console.error("Error fetching posts:", error);
    }
}

fetchPosts();

In this example, fetchPosts uses fetch() to get data from the API, waits for the response, converts it to JSON format, and logs the data.

Error Handling

Error handling is essential when working with asynchronous code. Use try...catch with async/await to handle errors gracefully, as shown in the example above.


Combining Multiple Asynchronous Tasks 🧩

Sometimes, you’ll need to run multiple asynchronous tasks. You can do this with Promise.all() to wait for all promises to complete, or Promise.race() to wait for only the first one.

Example: Using Promise.all()

async function fetchData() {
    let userPromise = fetch("https://jsonplaceholder.typicode.com/users/1");
    let postPromise = fetch("https://jsonplaceholder.typicode.com/posts?userId=1");

    let [user, posts] = await Promise.all([userPromise, postPromise]);

    let userData = await user.json();
    let postData = await posts.json();

    console.log("User:", userData);
    console.log("Posts:", postData);
}

fetchData();

In this example, we fetch user data and posts simultaneously, waiting for both to complete using Promise.all().


Practice Challenge: Fetch and Display Users 🎲

Let’s put these concepts to work by building a mini-app that fetches and displays users.

  1. Fetch a list of users from https://jsonplaceholder.typicode.com/users.
  2. Display each user’s name and email in the console.
  3. Handle any errors that may occur.

Example Solution

async function fetchAndDisplayUsers() {
    try {
        let response = await fetch("https://jsonplaceholder.typicode.com/users");
        let users = await response.json();

        users.forEach(user => {
            console.log(`Name: ${user.name}, Email: ${user.email}`);
        });
    } catch (error) {
        console.error("Error fetching users:", error);
    }
}

fetchAndDisplayUsers();

This code fetches the list of users and logs each user’s name and email, handling any potential errors.


Wrapping Up

In this part, we explored asynchronous JavaScript with callbacks, promises, and async/await. These techniques let you handle time-consuming tasks without slowing down your application. Next, in Part 9, we’ll explore error handling in depth and learn how to make robust, resilient applications.

Thanks for following along! Keep practicing, and your async skills will soon feel like second nature! 🎉