Chapter 3: Asynchronous Programming

Introduction

In JavaScript, many operations are asynchronous:

  • API calls (fetch)
  • File reading
  • Timers (setTimeout)
  • Local database access (IndexedDB)

Unlike PHP where code executes line by line, JavaScript can start an operation and continue without waiting for the result.

The Problem

// This code doesn't do what you think!
console.log("1. Start");

setTimeout(() => {
    console.log("2. Inside timeout");
}, 1000);

console.log("3. End");

// Output:
// "1. Start"
// "3. End"
// "2. Inside timeout" (1 second later)

Promises

A Promise represents a value that will be available in the future.

Promise States

  • pending: waiting
  • fulfilled: successfully resolved (contains a value)
  • rejected: rejected (contains an error)

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
    // Simulated asynchronous operation
    setTimeout(() => {
        const success = true;

        if (success) {
            resolve("Data received"); // Success
        } else {
            reject(new Error("Failure")); // Error
        }
    }, 1000);
});

Consuming a Promise with .then() and .catch()

myPromise
    .then(result => {
        console.log("Success:", result);
    })
    .catch(error => {
        console.log("Error:", error.message);
    });

// Chaining .then()
fetchUser(1)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts))
    .catch(error => console.log("Error:", error));

async/await

async/await is a more readable syntax for working with Promises.

Basic Syntax

// Async function
async function fetchData() {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
}

// Async arrow function
const fetchData = async () => {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    return data;
};

Important Rules

  1. await can only be used inside an async function
  2. An async function always returns a Promise
  3. await "pauses" execution until the Promise is resolved

Comparative Example

// With .then()
function getUserPosts(userId) {
    return fetchUser(userId)
        .then(user => fetchPosts(user.id))
        .then(posts => {
            console.log(posts);
            return posts;
        });
}

// With async/await (more readable)
async function getUserPosts(userId) {
    const user = await fetchUser(userId);
    const posts = await fetchPosts(user.id);
    console.log(posts);
    return posts;
}

Error Handling

With try/catch

async function fetchData() {
    try {
        const response = await fetch("https://api.example.com/data");

        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }

        const data = await response.json();
        return data;

    } catch (error) {
        console.error("Error:", error.message);
        // Handle error (display message, return default value, etc.)
        return null;
    }
}

Common Pattern in React

const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const loadData = async () => {
    setLoading(true);
    setError(null);

    try {
        const response = await fetch("/api/items");
        const result = await response.json();
        setData(result);
    } catch (err) {
        setError(err.message);
    } finally {
        setLoading(false);
    }
};

Sequential vs Parallel Execution

Sequential (one after another)

async function sequential() {
    const user = await fetchUser(1);      // Wait...
    const posts = await fetchPosts(1);    // Then wait...
    const comments = await fetchComments(1); // Then wait...

    // Total time = time1 + time2 + time3
}

Parallel (at the same time)

async function parallel() {
    // Start all 3 requests at the same time
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);

    // Total time = max(time1, time2, time3)
}

Promise.all vs Promise.allSettled

// Promise.all - fails if ONE promise fails
try {
    const results = await Promise.all([promise1, promise2, promise3]);
} catch (error) {
    // One of the promises failed
}

// Promise.allSettled - waits for all, even if some fail
const results = await Promise.allSettled([promise1, promise2, promise3]);
// results = [
//   { status: "fulfilled", value: ... },
//   { status: "rejected", reason: Error },
//   { status: "fulfilled", value: ... }
// ]

Comparison with PHP

// PHP - synchronous by default
$user = file_get_contents("https://api.example.com/user/1");
$posts = file_get_contents("https://api.example.com/posts/1");
// Each line waits for the previous one to finish
// JavaScript - asynchronous
const user = await fetch("https://api.example.com/user/1");
const posts = await fetch("https://api.example.com/posts/1");
// Similar thanks to await, but under the hood it's asynchronous

Common Pitfalls

1. Forgetting await

// INCORRECT
async function getData() {
    const data = fetch("/api/data"); // Forgot await!
    console.log(data); // Promise { <pending> } - not the data!
}

// CORRECT
async function getData() {
    const response = await fetch("/api/data");
    const data = await response.json();
    console.log(data);
}

2. await in a loop (unintended sequential)

// SLOW - each iteration waits for the previous one
async function slow() {
    for (const id of ids) {
        const data = await fetchItem(id); // Sequential!
    }
}

// FAST - all requests in parallel
async function fast() {
    const promises = ids.map(id => fetchItem(id));
    const results = await Promise.all(promises);
}

3. Not handling errors

// DANGEROUS - unhandled error
async function riskyCode() {
    const data = await fetchData(); // What if it fails?
}

// SAFE - error handled
async function safeCode() {
    try {
        const data = await fetchData();
    } catch (error) {
        console.error("Handled error:", error);
    }
}

Exercises

Exercise 1: Convert to async/await

Convert this code using .then():

function getUser(id) {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(user => {
            console.log(user);
            return user;
        })
        .catch(error => {
            console.error("Error:", error);
            return null;
        });
}

Solution:

async function getUser(id) {
    try {
        const response = await fetch(`/api/users/${id}`);
        const user = await response.json();
        console.log(user);
        return user;
    } catch (error) {
        console.error("Error:", error);
        return null;
    }
}

Exercise 2: Parallelization

Optimize this code to execute requests in parallel:

async function loadDashboard(userId) {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(userId);
    const notifications = await fetchNotifications(userId);

    return { user, orders, notifications };
}

Solution:

async function loadDashboard(userId) {
    const [user, orders, notifications] = await Promise.all([
        fetchUser(userId),
        fetchOrders(userId),
        fetchNotifications(userId)
    ]);

    return { user, orders, notifications };
}

Key Points to Remember

  1. Promise: represents a future value (pending -> fulfilled/rejected)
  2. async/await: readable syntax for Promises
  3. await: "pauses" execution until resolution
  4. try/catch: error handling with async/await
  5. Promise.all: execute multiple Promises in parallel
  6. Always handle errors with try/catch

<- Previous Chapter | Back to Module | Next Chapter: ES6 Modules ->