JavaScript Async/Await

Write asynchronous JavaScript that reads like synchronous code - async functions, await, try/catch, and parallel execution patterns.

Intermediate 11 min read 10 examples

async Function Basics

Any function can be made async. It automatically wraps the return value in a Promise.

JavaScript
// Regular function vs async function
function getAge()      { return 25; }            // returns 25
async function getAge2() { return 25; }          // returns Promise<25>

// All these are equivalent - async works with any function syntax
async function named()       { return 'a'; }
const arrowAsync = async () => 'b';
const exprAsync  = async function() { return 'c'; };

// The return value is always a Promise
const result = getAge2();
console.log(result instanceof Promise); // true
result.then(val => console.log(val));   // 25

// async functions with class methods
class UserService {
    async getUser(id) {
        const response = await fetch('/api/users/' + id);
        return response.json();
    }
}

// Calling an async function - it returns a Promise
const user = await new UserService().getUser(1);
// Or with .then()
new UserService().getUser(1).then(user => console.log(user));

The await Keyword

await pauses the async function and waits for the Promise to settle. The event loop continues running other tasks during the pause.

JavaScript
async function loadUserProfile(userId) {
    // await pauses here until fetch() Promise resolves
    const response = await fetch('/api/users/' + userId);

    // response is now the resolved value (a Response object)
    const user = await response.json(); // await again for the JSON parse

    console.log(user.name);
    return user; // async functions always return a Promise
}

// Sequential - each await waits for the previous
async function loadSequential() {
    const user    = await fetchUser(1);    // waits ~100ms
    const profile = await fetchProfile(user.id); // then waits ~100ms
    const posts   = await fetchPosts(user.id);   // then waits ~100ms
    // Total: ~300ms - slow because they run one after the other
    return { user, profile, posts };
}

// await works with any Promise, not just fetch
async function example() {
    await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1s
    const data = await Promise.resolve({ key: 'value' });    // instantly
    console.log(data.key); // 'value'
}

// await on a non-Promise just returns the value (harmless)
const x = await 42; // x === 42

Error Handling

Rejected Promises become thrown errors when awaited - catch them with a standard try/catch block.

JavaScript
// try/catch - catches both network errors and thrown errors inside
async function loadUser(id) {
    try {
        const response = await fetch('/api/users/' + id);
        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }
        return await response.json();
    } catch (err) {
        console.error('Failed to load user:', err.message);
        return null; // return a default instead of propagating the error
    }
}

// finally always runs
async function fetchWithCleanup(url) {
    showSpinner();
    try {
        const res = await fetch(url);
        return await res.json();
    } catch (err) {
        logError(err);
        throw err; // re-throw to let caller handle it too
    } finally {
        hideSpinner(); // runs whether success or failure
    }
}

// Granular error handling - different catch per step
async function processOrder(orderId) {
    let order;
    try {
        order = await fetchOrder(orderId);
    } catch {
        throw new Error('Order not found');
    }

    try {
        await chargePayment(order.total);
    } catch (err) {
        await cancelOrder(orderId);
        throw new Error('Payment failed: ' + err.message);
    }

    return sendConfirmation(order);
}

// .catch() shorthand for one-liners
const user = await fetchUser(1).catch(() => null);

Parallel Execution

Start multiple async operations without awaiting them immediately, then await all at once with Promise.all().

JavaScript
// SLOW: sequential - each waits for the previous
async function slowDashboard() {
    const user     = await fetchUser();     // 200ms
    const posts    = await fetchPosts();    // 200ms
    const comments = await fetchComments();// 200ms
    // Total: ~600ms
}

// FAST: parallel - all three start simultaneously
async function fastDashboard() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(),     // starts immediately
        fetchPosts(),    // starts immediately
        fetchComments(), // starts immediately
    ]);
    // Total: ~200ms (longest single request)
    render(user, posts, comments);
}

// Start manually without await, then collect
async function manualParallel() {
    const userPromise  = fetchUser();  // starts now
    const postsPromise = fetchPosts(); // starts now

    // Do some synchronous work...
    const config = loadConfig();

    // Now collect the async results
    const user  = await userPromise;
    const posts = await postsPromise;
    return { user, posts, config };
}

// allSettled - parallel but handles individual failures
async function robustLoad(ids) {
    const results = await Promise.allSettled(ids.map(id => fetchUser(id)));
    return results
        .filter(r => r.status === 'fulfilled')
        .map(r => r.value);
}
await in a loop runs sequentially

A for loop with await runs each iteration after the previous one finishes - they are sequential, not parallel. To process an array of items in parallel, use await Promise.all(arr.map(async item => ...)). Only use the sequential loop when order matters or you need to limit concurrency.

Async Iteration Patterns

JavaScript
// Sequential: for...of with await (order preserved, one at a time)
async function processInOrder(items) {
    for (const item of items) {
        await process(item); // each waits for previous
    }
}

// Parallel: map + Promise.all (all at once)
async function processInParallel(items) {
    await Promise.all(items.map(item => process(item)));
}

// Controlled concurrency (e.g., max 3 at a time)
async function processWithLimit(items, limit = 3) {
    const results = [];
    for (let i = 0; i < items.length; i += limit) {
        const batch = items.slice(i, i + limit);
        const batchResults = await Promise.all(batch.map(process));
        results.push(...batchResults);
    }
    return results;
}

// async for...of with an async iterable (streams, paginated APIs)
async function* paginate(url) {
    let nextUrl = url;
    while (nextUrl) {
        const res  = await fetch(nextUrl);
        const data = await res.json();
        yield data.items;
        nextUrl = data.nextPage ?? null;
    }
}

for await (const items of paginate('/api/products')) {
    renderBatch(items);
}

Top-Level await

In ES modules (type="module" or .mjs files), you can use await at the top level without wrapping in an async function.

JavaScript
// In an ES module (type="module" script or .mjs file)

// Load config before the rest of the module executes
const config = await fetch('/api/config').then(r => r.json());
console.log('Config loaded:', config.version);

// Initialize database connection
const db = await connectToDatabase(config.dbUrl);

// Conditional import
const { default: polyfill } = await import('./polyfill.js');

// Export values that depend on async initialization
export { db, config };

// Modules that import this file wait for the top-level awaits to resolve
// import { db } from './db.mjs'; // this line waits for db to be ready

// NOTE: top-level await is NOT available in classic scripts (no type="module")
// and in CommonJS (require()). It pauses the entire module until resolved.

Common Pitfalls

JavaScript
// Pitfall 1: await in forEach (doesn't wait)
async function bad() {
    const ids = [1, 2, 3];
    ids.forEach(async id => {
        await processId(id); // forEach doesn't await these!
    });
    console.log('done'); // prints before processing finishes
}

// Fix: use for...of
async function good() {
    for (const id of [1, 2, 3]) {
        await processId(id);
    }
    console.log('done'); // prints after all processing
}

// Pitfall 2: not awaiting an async function - silent bug
async function saveUser(user) { /* ... */ }

async function createAndSave() {
    const user = buildUser();
    saveUser(user);     // forgot await! errors are silently swallowed
    console.log('saved'); // prints immediately even if saveUser fails
}

// Fix:
async function createAndSave2() {
    const user = buildUser();
    await saveUser(user);
    console.log('saved');
}

// Pitfall 3: mixing async/await and .then() chains - hard to read
async function mixed() {
    const data = await fetch('/api/data')
        .then(r => r.json())          // mixing await + .then
        .then(d => d.filter(Boolean)); // confusing!
    return data;
}

// Better: consistent style
async function consistent() {
    const res  = await fetch('/api/data');
    const json = await res.json();
    return json.filter(Boolean);
}

Frequently Asked Questions

Adding async before a function does two things: (1) it allows the await keyword to be used inside the function, and (2) it wraps the return value in a Promise automatically. If the function returns 42, callers receive Promise.resolve(42). If it throws an error, callers receive a rejected Promise. You cannot use await outside an async function (except at the top level of a module).

await pauses execution of the current async function until the Promise it is awaiting settles, then resumes with the resolved value. The JavaScript event loop is NOT blocked during this pause - other code can run. If the Promise rejects, await throws the rejection reason, which can be caught with try/catch.

Use await Promise.all([p1, p2, p3]). Starting each async call without awaiting it immediately gives you a Promise reference. Passing all Promise references to Promise.all() and then awaiting that runs them concurrently. Awaiting each call sequentially (await fetch1(); await fetch2();) runs them one after the other - each waits for the previous to finish.

Not effectively. Array.forEach does not await async callbacks - it fires them all and moves on. Use for...of with await for sequential execution, or await Promise.all(arr.map(async item => ...)) for parallel execution. Never use forEach with async callbacks expecting sequential behavior.