• JavaScript
    • Node.js
    • Tutorial
    • Beginner

    Async/Await in Node.js: A Practical Guide

    By Published Updated 4 min read

    Practical async/await patterns for Node.js: sequential steps, Promise.all, Promise.allSettled, error handling and top-level await.

    async/await lets you write asynchronous Node.js code that reads top to bottom like synchronous code, while still never blocking the event loop. An async function always returns a promise, and await pauses that function (not the whole program) until a promise settles. This guide covers the patterns you'll use daily: sequential steps, parallel work with Promise.all, partial failures with Promise.allSettled, and error handling.

    Key takeaways

    • async/await is built on promises. An async function returns a promise, and await unwraps one.
    • Awaiting independent tasks one after another makes them run sequentially. Start them together and use Promise.all when they don't depend on each other.
    • Promise.all rejects as soon as any input rejects. Promise.allSettled waits for every result, successful or not.
    • Wrap awaits in try/catch (or attach .catch()) so rejections don't go unhandled.
    • Top-level await works in ES modules in Node.js, and in the WebFiddle Node.js sandbox.

    From callbacks to async/await

    JavaScript runs your code on a single thread, but real applications constantly wait: for files, databases, APIs and email servers. Node.js handles this by starting the operation and running your code again when the result is ready, so one slow request doesn't stall everyone else. Our article on the Node.js event loop explains the mechanics.

    Callbacks

    Early Node.js APIs reported results through callbacks. Nesting them for each step leads to the "pyramid of doom":

    getUser(userId, (err, user) => {
        if (err) return handle(err);
        getPosts(user.id, (err, posts) => {
            if (err) return handle(err);
            getComments(posts[0].id, (err, comments) => {
                if (err) return handle(err);
                // Every step adds another level of nesting
            });
        });
    });
    

    Promises

    ES2015 (ES6) standardised promises, which let you chain steps with .then() and handle errors in one .catch(). That was a big improvement, but long chains are still noisy.

    async/await

    async functions were standardised in ES2017. V8 5.5 shipped async functions, and Node.js enabled them without a flag when Node.js 7.6.0 upgraded to V8 5.5. Every current Node.js release supports them. MDN's async function reference covers the full syntax.

    A tiny helper for the examples

    The examples below use a fake "database" built from timers, so you can paste them straight into a sandbox:

    const sleep = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    const fail = (ms, message) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
    
    const db = {
        getUser: (id) => sleep(300, { id, name: 'Ada' }),
        getPosts: (userId) => sleep(300, ['First post', 'Second post']),
        getStats: () => sleep(300, { visits: 42 }),
    };
    

    1. Sequential execution

    Use this when each step needs the result of the previous one.

    async function processUser(userId) {
        try {
            const user = await db.getUser(userId);     // pauses this function ~300 ms
            const posts = await db.getPosts(user.id);  // needs user.id, so it must wait
            console.log(`${user.name} has ${posts.length} posts`);
            return posts;
        } catch (error) {
            console.error('Something went wrong:', error.message);
            throw error; // let the caller decide what to do
        }
    }
    
    await processUser(1);
    

    Pros: easy to read and debug. Cons: the delays add up. Two 300 ms steps take about 600 ms in total.

    2. Parallel execution with Promise.all

    A common mistake is awaiting independent tasks one by one. Start them first, then wait for all of them together:

    async function getDashboardData() {
        const start = Date.now();
    
        // Both operations start immediately
        const userPromise = db.getUser(1);
        const statsPromise = db.getStats();
    
        // Wait for both to finish
        const [user, stats] = await Promise.all([userPromise, statsPromise]);
    
        console.log(user.name, stats.visits, `${Date.now() - start} ms`);
        return { user, stats };
    }
    
    await getDashboardData();
    

    Two 300 ms operations now take roughly 300 ms, because they wait at the same time. They are concurrent, not running on separate CPU threads, which is ideal for I/O. Note that Promise.all rejects as soon as any of its promises rejects, and you lose the other results.

    3. Partial failures with Promise.allSettled

    When some results are optional, use Promise.allSettled (available since Node.js 12.9.0). It never rejects. Instead, it reports the outcome of every promise:

    const results = await Promise.allSettled([
        sleep(100, 'User profile'),
        fail(100, 'News feed is down'),
    ]);
    
    for (const result of results) {
        if (result.status === 'fulfilled') {
            console.log('OK:', result.value);
        } else {
            console.log('Failed:', result.reason.message);
        }
    }
    // OK: User profile
    // Failed: News feed is down
    

    This suits dashboards where you still want to show the profile even if the news feed API failed.

    4. Error handling

    A rejected promise makes await throw, so ordinary try/catch works. Two habits prevent most bugs:

    • Don't forget await. try { doAsync(); } catch {} catches nothing, because the rejection happens after the try block has finished.
    • Handle every rejection. Since Node.js 15, an unhandled promise rejection crashes the process by default.
    async function loadSettings() {
        try {
            return await fail(50, 'Settings service timed out');
        } catch (error) {
            console.warn('Using defaults:', error.message);
            return { theme: 'light' };
        }
    }
    
    console.log(await loadSettings());
    

    Top-level await

    In Node.js, you can use await outside a function in ES modules: files ending in .mjs, or .js files in a package with "type": "module". The Node.js ESM documentation covers the details. CommonJS files don't support it.

    // index.mjs
    const response = await fetch('https://api.github.com/zen');
    console.log(await response.text());
    

    The global fetch is available by default from Node.js 18.

    Practise in the browser

    Every example above except the index.mjs file runs as-is in the WebFiddle Node.js sandbox. The sandbox wraps your code in an async function, so top-level await works without any setup. It runs in a Web Worker with a Node.js API shim rather than a real Node.js process, so require() and npm packages aren't available. See how we run Node.js code in the browser for details.

    Frequently asked questions

    What version of Node.js supports async/await?

    Node.js 7.6.0, released in February 2017, was the first version to enable async functions by default, after upgrading to the V8 5.5 engine. Earlier 7.x releases supported them only behind a flag. Every Node.js version still receiving updates today supports async/await, as well as top-level await in ES modules.

    Does await block the Node.js event loop?

    No. await pauses only the async function it appears in. While that function waits for a promise, the event loop keeps running other callbacks, timers and incoming requests. When the promise settles, the rest of the function is scheduled as a microtask. CPU-heavy synchronous code inside an async function still blocks the loop.

    What is the difference between Promise.all and Promise.allSettled?

    Promise.all resolves with an array of values when every promise fulfils, but rejects immediately if any one of them rejects. Promise.allSettled always waits for every promise and resolves with an array of objects, each with a status of fulfilled or rejected plus its value or reason, so one failure doesn't hide the other results.

    How do I handle errors with async/await?

    Wrap the awaited calls in a try/catch block. A rejected promise makes await throw, so the catch block receives the error just like a synchronous exception. Always await the promise inside the try block, and either handle the error or rethrow it so callers know something failed. Unhandled rejections crash modern Node.js.

    Can I use await at the top level in Node.js?

    Yes, in ES modules. Top-level await works in files ending in .mjs, or in .js files when your package.json sets type to module. CommonJS scripts don't support it, so wrap the code in an async function there. The WebFiddle Node.js sandbox allows top-level await because it wraps your code automatically.

    Try the examples yourself

    Open the Node.js sandbox to run and edit code from this article in your browser. There is nothing to install and no account to create.