Skip to main content

JavaScript Async/Await: A Comprehensive Guide

Table of Contents

Async/Await in JavaScript
#

Async/await is syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code.

Async Functions
#

An async function always returns a Promise:

// Async function declaration
async function fetchData() {
    return "data";
}

// Equivalent to:
function fetchData() {
    return Promise.resolve("data");
}

// Using async functions
fetchData().then(data => {
    console.log(data); // "data"
});

// Async arrow function
const getData = async () => {
    return "data";
};

Await Keyword
#

Await pauses execution until a Promise is resolved:

async function fetchUser(id) {
    // Wait for the fetch to complete
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user;
}

// Without await (for comparison)
function fetchUser(id) {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(user => user);
}

Error Handling
#

Use try/catch for error handling with async/await:

async function fetchData() {
    try {
        const response = await fetch('/api/data');
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to fetch data:', error);
        throw error; // Re-throw if needed
    }
}

// Using the function
async function main() {
    try {
        const data = await fetchData();
        console.log('Data:', data);
    } catch (error) {
        console.log('Error in main:', error.message);
    }
}

Multiple Async Operations
#

Handle multiple async operations efficiently:

// Sequential execution (slower)
async function sequential() {
    const user = await fetchUser(1);
    const posts = await fetchPosts(user.id);
    const comments = await fetchComments(posts[0].id);
    return { user, posts, comments };
}

// Parallel execution (faster)
async function parallel() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(1),
        fetchPosts(1),
        fetchComments(1)
    ]);
    return { user, posts, comments };
}

// Race - returns first resolved promise
async function fastest() {
    const result = await Promise.race([
        fetchFromServer1(),
        fetchFromServer2(),
        fetchFromServer3()
    ]);
    return result;
}

// allSettled - wait for all regardless of success/failure
async function allResults() {
    const results = await Promise.allSettled([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ]);
    
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`User ${index}:`, result.value);
        } else {
            console.log(`User ${index} failed:`, result.reason);
        }
    });
}

Async Iteration
#

Process async operations in loops:

// Using for...of with await
async function processUsers(userIds) {
    for (const id of userIds) {
        const user = await fetchUser(id);
        console.log(user);
    }
}

// Using for await...of with async iterables
async function* asyncGenerator() {
    yield await Promise.resolve(1);
    yield await Promise.resolve(2);
    yield await Promise.resolve(3);
}

async function consume() {
    for await (const value of asyncGenerator()) {
        console.log(value);
    }
}

// Parallel processing with map
async function processAllUsers(userIds) {
    const promises = userIds.map(id => fetchUser(id));
    const users = await Promise.all(promises);
    return users;
}

Top-Level Await
#

Use await at the top level in modules:

// In ES modules (with "type": "module" in package.json)
const response = await fetch('/api/config');
const config = await response.json();

export const API_URL = config.apiUrl;
export const TIMEOUT = config.timeout;

// Top-level await blocks module loading
// Dependents wait until this module resolves

Best Practices
#

Tips for effective async/await usage:

// 1. Always return from async functions
async function good() {
    const data = await fetch('/api/data');
    return data; // Explicit return
}

// 2. Avoid mixing async/await with .then()
// Bad
async function mixed() {
    return await fetch('/api/data').then(r => r.json());
}

// Good
async function clean() {
    const response = await fetch('/api/data');
    return await response.json();
}

// 3. Use Promise.all for independent operations
async function optimized() {
    const [users, products] = await Promise.all([
        fetchUsers(),
        fetchProducts()
    ]);
    return { users, products };
}

// 4. Create utility functions for common patterns
async function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Timeout')), ms)
    );
    return Promise.race([promise, timeout]);
}

// Usage
const data = await withTimeout(fetchData(), 5000);

Related