DevRef / JavaScript / Async

Async/Await Patterns in Modern JavaScript

Async/await did not replace Promises — it is built on top of them. Understanding what the runtime is actually doing when it suspends and resumes execution makes it much easier to avoid the subtle bugs that async code tends to produce.

What async/await is

An async function always returns a Promise. The await keyword inside it pauses execution of that function until the awaited Promise settles, then resumes with the resolved value. Control returns to the caller during the pause — this is not blocking.

async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error('User not found');
    return response.json();
}

The returned Promise resolves with whatever the async function returns, or rejects with whatever it throws. Callers treat it exactly like any other Promise — they can await it or chain .then().

Error handling

Inside an async function, try/catch wraps both synchronous and asynchronous errors uniformly. This is one of the main ergonomic improvements over chained Promises, where error handling required a .catch() at each step or a single broad handler that could mask the source of an error.

async function loadProfile(userId) {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchPosts(user.id);
        return { user, posts };
    } catch (err) {
        console.error('Profile load failed:', err.message);
        throw err; // re-throw if the caller needs to handle it
    }
}

Note that await only catches Promise rejections, not synchronous throws that happen before the first await. A synchronous throw at the top of an async function does reject the returned Promise, but only because the async function machinery wraps the entire body.

Parallelism: Promise.all

Sequential awaits are sequential — each waits for the previous to resolve. When operations are independent, use Promise.all to run them concurrently.

// Sequential — user fetched, THEN posts fetched
const user = await fetchUser(id);
const posts = await fetchPosts(id);

// Parallel — both in-flight simultaneously
const [user, posts] = await Promise.all([
    fetchUser(id),
    fetchPosts(id),
]);

Promise.all rejects as soon as any Promise in the array rejects. If you want all results regardless of individual failures, use Promise.allSettled, which returns an array of outcome objects with a status field.

Common pitfall: missing await

Omitting await before an async call returns the Promise object, not the resolved value. This is valid JavaScript — it simply means you are working with the Promise directly. TypeScript catches this more reliably than linters, since it can track whether a value is a Promise<T> or a bare T.