Async/Await

May 18, 2026
#javascript #js #async #await #promises

Async/await is syntactic sugar on top of Promises. It lets you write asynchronous code that reads like synchronous code — no more .then() chains. Under the hood, it’s still promises, but the code is dramatically easier to read and reason about.

The Basics

Mark a function as async, then use await inside it to pause until a promise resolves:

async function getUser(id) {
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
}

// Calling an async function returns a promise
getUser(1).then(user => console.log(user.name));

Compare this to the promise chain version:

function getUser(id) {
    return fetch(`/api/users/${id}`)
        .then(response => response.json());
}

For simple cases, they’re similar. The advantage of async/await becomes clear with more complex flows.

Sequential Operations

When each step depends on the previous one, async/await shines:

async function getOrderSummary(userId) {
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();

    const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
    const orders = await ordersResponse.json();

    const latestOrder = orders[0];
    const detailsResponse = await fetch(`/api/orders/${latestOrder.id}`);
    const details = await detailsResponse.json();

    return {
        userName: user.name,
        orderCount: orders.length,
        latestOrderTotal: details.total
    };
}

The same thing with .then() chains would be much harder to follow.

Error Handling with try/catch

Use standard try/catch blocks — no more .catch() callbacks:

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);

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

        const user = await response.json();
        return user;
    } catch (error) {
        console.error("Failed to fetch user:", error.message);
        return null;
    }
}

try/catch with multiple operations

async function processPayment(orderId, paymentDetails) {
    try {
        const order = await getOrder(orderId);
        const validation = await validatePayment(paymentDetails);
        const result = await chargeCard(validation.token, order.total);
        await sendConfirmationEmail(order.email, result.transactionId);

        return { success: true, transactionId: result.transactionId };
    } catch (error) {
        // Any failure in the chain lands here
        await logPaymentError(orderId, error);
        return { success: false, error: error.message };
    } finally {
        // Runs regardless of success or failure
        releasePaymentLock(orderId);
    }
}

Parallel Execution

A common mistake: using await sequentially when operations are independent.

// Bad — these run one after another (slow)
async function getDashboardData() {
    const users = await fetch("/api/users").then(r => r.json());
    const posts = await fetch("/api/posts").then(r => r.json());
    const stats = await fetch("/api/stats").then(r => r.json());
    return { users, posts, stats };
}

// Good — all three run in parallel (fast)
async function getDashboardData() {
    const [users, posts, stats] = await Promise.all([
        fetch("/api/users").then(r => r.json()),
        fetch("/api/posts").then(r => r.json()),
        fetch("/api/stats").then(r => r.json())
    ]);
    return { users, posts, stats };
}

If the three requests each take 200ms:

  • Sequential: ~600ms total
  • Parallel: ~200ms total

Use await Promise.all() whenever operations don’t depend on each other.

Loops and Async

Sequential processing (one at a time)

async function processItems(items) {
    const results = [];

    for (const item of items) {
        const result = await processItem(item);  // waits for each one
        results.push(result);
    }

    return results;
}

Use this when order matters or when you’d overwhelm a server with parallel requests.

Parallel processing (all at once)

async function processItems(items) {
    const results = await Promise.all(
        items.map(item => processItem(item))
    );
    return results;
}

Use this when items are independent and you want maximum speed.

Controlled concurrency (batches)

async function processInBatches(items, batchSize = 5) {
    const results = [];

    for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchResults = await Promise.all(
            batch.map(item => processItem(item))
        );
        results.push(...batchResults);
    }

    return results;
}

Async Arrow Functions

// Named async function
async function fetchData() { /* ... */ }

// Async arrow function
const fetchData = async () => { /* ... */ };

// Async method in an object
const api = {
    async getUser(id) {
        const response = await fetch(`/api/users/${id}`);
        return response.json();
    }
};

// Async IIFE (immediately invoked)
(async () => {
    const data = await fetchData();
    console.log(data);
})();

Real-World Patterns

Fetch wrapper with error handling

async function api(endpoint, options = {}) {
    const baseUrl = "https://api.example.com";

    const response = await fetch(`${baseUrl}${endpoint}`, {
        headers: { "Content-Type": "application/json" },
        ...options
    });

    if (!response.ok) {
        const error = await response.json().catch(() => ({}));
        throw new Error(error.message || `HTTP ${response.status}`);
    }

    return response.json();
}

// Usage
async function createUser(userData) {
    try {
        const user = await api("/users", {
            method: "POST",
            body: JSON.stringify(userData)
        });
        return user;
    } catch (error) {
        console.error("Failed to create user:", error.message);
        throw error;
    }
}

Retry with exponential backoff

async function fetchWithRetry(url, maxRetries = 3) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (error) {
            if (attempt === maxRetries) throw error;

            const delay = Math.pow(2, attempt) * 1000;  // 1s, 2s, 4s
            console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

Loading state management

async function loadData(setState) {
    setState({ loading: true, error: null });

    try {
        const data = await api("/data");
        setState({ loading: false, data, error: null });
    } catch (error) {
        setState({ loading: false, data: null, error: error.message });
    }
}

Common Mistakes

Forgetting await

// Bug — response is a Promise, not the actual response
async function getUser() {
    const response = fetch("/api/user");  // missing await!
    const user = response.json();         // TypeError: response.json is not a function
}

Using await at the top level (without a module)

// This only works in ES modules or modern environments
const data = await fetch("/api/data");  // SyntaxError in non-module scripts

// Workaround for non-module scripts
(async () => {
    const data = await fetch("/api/data");
})();

Swallowing errors silently

// Bad — error disappears
async function getData() {
    try {
        return await fetch("/api/data").then(r => r.json());
    } catch (error) {
        // silently returns undefined
    }
}

// Good — handle or re-throw
async function getData() {
    try {
        return await fetch("/api/data").then(r => r.json());
    } catch (error) {
        console.error("getData failed:", error);
        throw error;  // let the caller handle it
    }
}

When to Use What

Situation Approach
Simple single async call Either .then() or async/await
Multiple sequential steps async/await (much cleaner)
Parallel independent operations await Promise.all([...])
Error handling with recovery try/catch in async function
Event handlers async callback: btn.addEventListener("click", async () => {...})
Top-level script Async IIFE or ES module top-level await