advanced Step 13 of 20

Async/Await and Fetch

JavaScript Programming

Async/Await and Fetch

Async/await is syntactic sugar built on top of Promises that makes asynchronous code look and behave more like synchronous code. Introduced in ES2017, async/await has become the standard way to write asynchronous JavaScript. Combined with the Fetch API — the modern replacement for XMLHttpRequest — you have a clean, powerful toolkit for making HTTP requests and handling responses. Async/await eliminates the need for .then() chains while keeping all the benefits of Promises.

Async Functions

// An async function always returns a Promise
async function greet() {
    return "Hello!";  // Automatically wrapped in Promise.resolve()
}
greet().then(msg => console.log(msg));  // "Hello!"

// await pauses execution until the Promise resolves
async function fetchUser(id) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
    const user = await response.json();
    return user;
}

// Calling an async function
async function main() {
    const user = await fetchUser(1);
    console.log(user.name);  // "Leanne Graham"
}
main();

// Error handling with try/catch
async function safeFetch(url) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Fetch failed:", error.message);
        return null;
    }
}

The Fetch API

// GET request
async function getUsers() {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const users = await response.json();
    return users;
}

// POST request
async function createPost(postData) {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(postData)
    });
    const newPost = await response.json();
    return newPost;
}

// PUT request
async function updatePost(id, data) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data)
    });
    return response.json();
}

// DELETE request
async function deletePost(id) {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
        method: "DELETE"
    });
    return response.ok;
}

// Usage
const newPost = await createPost({
    title: "My Post",
    body: "Post content",
    userId: 1
});
console.log("Created:", newPost);

Parallel vs Sequential Execution

// SEQUENTIAL — each waits for the previous one (slower)
async function sequential() {
    const users = await fetch("/api/users").then(r => r.json());
    const posts = await fetch("/api/posts").then(r => r.json());
    const comments = await fetch("/api/comments").then(r => r.json());
    return { users, posts, comments };
    // Total time: users + posts + comments
}

// PARALLEL — all requests start at the same time (faster)
async function parallel() {
    const [users, posts, comments] = await Promise.all([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/posts").then(r => r.json()),
        fetch("/api/comments").then(r => r.json())
    ]);
    return { users, posts, comments };
    // Total time: max(users, posts, comments)
}

// Parallel with error handling for each
async function parallelSafe() {
    const results = await Promise.allSettled([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/posts").then(r => r.json()),
        fetch("/api/maybe-fails").then(r => r.json())
    ]);

    const [users, posts, other] = results.map(r =>
        r.status === "fulfilled" ? r.value : null
    );

    return { users, posts, other };
}

Practical Fetch Wrapper

// Reusable API client
class ApiClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }

    async request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;
        const config = {
            headers: { "Content-Type": "application/json" },
            ...options,
            body: options.body ? JSON.stringify(options.body) : undefined
        };

        try {
            const response = await fetch(url, config);
            if (!response.ok) {
                throw new Error(`API Error: ${response.status}`);
            }
            return await response.json();
        } catch (error) {
            console.error(`Request failed: ${error.message}`);
            throw error;
        }
    }

    get(endpoint)           { return this.request(endpoint); }
    post(endpoint, body)    { return this.request(endpoint, { method: "POST", body }); }
    put(endpoint, body)     { return this.request(endpoint, { method: "PUT", body }); }
    delete(endpoint)        { return this.request(endpoint, { method: "DELETE" }); }
}

// Usage
const api = new ApiClient("https://jsonplaceholder.typicode.com");
const users = await api.get("/users");
const newPost = await api.post("/posts", { title: "Hello", body: "World", userId: 1 });
Pro tip: Always check response.ok after a fetch call. The Fetch API does NOT reject the promise on HTTP errors (404, 500, etc.) — it only rejects on network failures. You must manually check the status code and throw an error for non-2xx responses.

Key Takeaways

  • Async functions return Promises; await pauses execution until the Promise resolves.
  • Use try/catch for error handling in async functions instead of .catch() chains.
  • The Fetch API returns a Response object; call .json() to parse the body (also returns a Promise).
  • Use Promise.all() for parallel requests when tasks are independent — it is significantly faster than sequential awaits.
  • Always check response.ok because fetch does not throw on HTTP error status codes.