search

JavaScript async/await Explained: A Human Guide

I use async and await because they make asynchronous JavaScript readable. Not magical. Not faster by default. Just easier to follow when a request, a database call, or a WordPress REST API endpoint needs time to answer.

The way I think about it is simple: Promises are still doing the work underneath. async/await just lets me write that Promise-based code in the order I actually think about it.

What async Does

When you mark a function as async, that function always returns a Promise. Even if you return a plain value, JavaScript wraps it for you.

async function getNumber() {
    return 42;
}

getNumber().then(function (number) {
    console.log(number); // 42
});

If an async function throws an error, the returned Promise rejects. That is the first important mental shift: once a function is async, callers need to treat it like a Promise-returning function.

An async function declaration creates a function that returns a new Promise, resolving with the function’s return value or rejecting with an uncaught exception. MDN Web Docs

What await Really Does

await pauses the current async function until the Promise settles. It does not freeze the browser. It does not block the whole JavaScript thread. The rest of the page can keep working.

console.log('1');

async function run() {
    console.log('2');
    const value = await Promise.resolve('done');
    console.log('4', value);
}

run();
console.log('3');

The output is 1, 2, 3, 4 done. The function pauses at await, returns control to the rest of the program, and continues later.

That is the part I wish more developers understood early. await makes the code look linear, but the runtime is still asynchronous.

Why I Prefer try/catch

The biggest practical win is error handling. Promise chains can be perfectly valid, but once a flow has several steps, I find try/catch much easier to read and debug.

async function loadDashboard(userId) {
    try {
        const user = await fetchUser(userId);
        const orders = await fetchOrders(user.id);
        return { user, orders };
    } catch (err) {
        console.error('Dashboard failed', err);
        return { user: null, orders: [] };
    }
}

This reads like the flow I have in my head: get the user, get the orders, return the result, handle anything that failed. For more detail on the mechanics, my try…catch guide covers the error-handling side separately.

Do not swallow errors silently. An empty catch block makes async bugs much harder to find. Log the error, recover from it, or throw it again.

The Performance Trap

The one async/await mistake I see most often is accidental sequential code: three independent requests written as three separate await lines. It looks clean, but it makes the browser wait longer than needed.

I moved that topic into a shorter, focused post with live demos: Sequential vs Parallel JavaScript: The Performance Mistake.

The short version: if one async operation needs the result of the previous one, keep it sequential. If the operations are independent, start them together with Promise.all().

The forEach Trap

This is the other mistake worth calling out because it looks so innocent:

async function saveAll(items) {
    items.forEach(async function (item) {
        await save(item);
    });

    console.log('done?');
}

forEach does not wait for the async callback. It ignores the Promise returned by that callback, so done? can run before the saves finish.

Use for...of when the work must happen one item at a time:

async function saveAll(items) {
    for (const item of items) {
        await save(item);
    }

    console.log('done');
}

Use Promise.all() with map() when the items can be processed together:

async function saveAll(items) {
    await Promise.all(items.map(function (item) {
        return save(item);
    }));

    console.log('done');
}

A Real WordPress Shape

In WordPress frontend code, I usually see async/await around the REST API. A common pattern is loading a post, then loading data that depends on that post.

async function loadPostBundle(postId) {
    const base = '/wp-json/wp/v2';

    try {
        const post = await fetch(base + '/posts/' + postId)
            .then(function (response) { return response.json(); });

        const author = await fetch(base + '/users/' + post.author)
            .then(function (response) { return response.json(); });

        return { post, author };
    } catch (err) {
        console.error('Could not load post bundle', err);
        return null;
    }
}

This is sequential on purpose. The author request needs post.author, so I wait for the post first. When you render the result, remember that the DOM work still matters. The guide on avoiding excessive DOM size is a good next step if you are building dynamic interfaces.

Common Mistakes

These are the async/await issues I look for first in real code:

  • Forgetting await. You get a Promise instead of the value you expected.
  • Using await inside forEach. The loop does not wait.
  • Writing empty catch blocks. The failure disappears until users report it.
  • Making independent requests sequential. That performance mistake now has its own focused post with live demos.
  • Adding async when nothing awaits. Sometimes returning the original Promise is clearer.

FAQs

Common questions about using async and await in JavaScript.

What does an async function return?
An async function always returns a Promise. A returned value becomes a resolved Promise, and a thrown error becomes a rejected Promise.
Does await block JavaScript?
No. await pauses the current async function and gives control back to the event loop. It does not freeze the browser.
Should I always use try/catch with await?
Use try/catch when you can handle the failure at that point. If the caller should decide what to do, let the rejection bubble up.
Can I use await inside forEach?
You can write it, but forEach will not wait for the async callback. Use for...of for sequential work or Promise.all() with map() for parallel work.
When should I use Promise.all?
Use Promise.all() when several async operations are independent and you need all of them before continuing.

Summary

async/await is not a different engine under the hood. It is a clearer way to write Promise-based code.

Use async when a function should return a Promise. Use await when you need to pause that function until a Promise settles. Use try/catch where you can actually handle failures. And when performance matters, make sure you are not waiting in sequence for work that could have started together.

Join the Discussion
0 Comments  ]

Leave a Comment

To add code, use the buttons below. For instance, click the PHP button to insert PHP code within the shortcode. If you notice any typos, please let us know!

Savvy WordPress Development official logo