Async/await made asynchronous JavaScript far easier to read. Code that once required nested callbacks or long promise chains now looks almost synchronous. But that readability can be deceptive. Under the hood you are still working with promises, and a handful of mistakes show up again and again in code reviews. Here are seven of them, with the fix for each.

1. Forgetting to await

The most common bug is calling an async function and forgetting the await. You then get a pending promise instead of the value you wanted, and the rest of your code runs before the work is finished.

async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

const user = getUser(5);      // a Promise, not a user!
console.log(user.name);       // undefined

The fix is to add await (inside an async function) or use .then(). Linters can help: the @typescript-eslint/no-floating-promises rule and similar checks flag promises that are created and then ignored.

2. Running independent tasks one after another

Writing await in a loop or on consecutive lines makes each task wait for the previous one, even when they do not depend on each other. If you need a user, their posts, and their settings, fetching them sequentially triples your waiting time.

// Slow: three round trips in a row
const user = await getUser(id);
const posts = await getPosts(id);
const settings = await getSettings(id);

// Faster: start all three, then wait for all
const [user, posts, settings] = await Promise.all([
  getUser(id),
  getPosts(id),
  getSettings(id),
]);

Use Promise.all when every result is needed and any failure should stop the whole operation. Use Promise.allSettled when you want every result regardless of individual failures.

3. Using await inside forEach

This one surprises almost everyone. Array.prototype.forEach does not wait for the promises returned by an async callback. The loop finishes immediately and your code moves on while the work is still running.

// Does NOT wait for each save to finish
items.forEach(async (item) => {
  await save(item);
});
console.log("done"); // runs too early

If you need to run tasks in order, use a for...of loop with await. If order does not matter, map to an array of promises and use Promise.all:

for (const item of items) {
  await save(item);        // one at a time, in order
}

await Promise.all(items.map((item) => save(item))); // all at once

Be careful with the parallel version when items is large, since firing thousands of requests at once can overwhelm a server or hit rate limits. In that case, process them in batches.

4. Swallowing errors

A rejected promise inside an async function throws when you await it, so you can catch it with an ordinary try/catch. The mistake is catching the error and then doing nothing useful with it.

try {
  await saveProfile(data);
} catch (err) {
  console.log("something went wrong"); // lost the details, user sees nothing
}

An empty or vague catch block hides the real cause and leaves the user staring at a UI that never updates. At the very least, log the actual error. Better, decide what the user should see, and either recover, retry, or rethrow so a higher level can handle it.

5. Unhandled promise rejections

If an async function throws and nobody awaits or catches its promise, you get an unhandled rejection. In Node.js this can crash the process; in browsers it appears as a console error and the failure goes unnoticed. This often happens with "fire and forget" calls.

button.addEventListener("click", () => {
  saveProfile(data);   // if this rejects, nobody handles it
});

Attach a .catch() handler, or wrap the body in try/catch. Even for background work you intentionally do not wait for, handle the failure explicitly so it is logged somewhere you will see it.

6. Mixing async/await with .then() carelessly

You can combine both styles, but mixing them in the same function leads to confusing control flow and missed returns. A common slip is forgetting to return the promise inside a .then(), so the outer await finishes early.

async function load() {
  await fetch("/api/a").then((res) => {
    res.json();            // missing return: result is lost
  });
}

Pick one style per function. Async/await is usually clearer, so convert chains when you can.

7. Marking everything async by default

Declaring a function async when it never awaits anything is harmless to behavior but misleading to readers, and it forces every caller to deal with a promise. An async function always returns a promise, so changing a synchronous function to async is a breaking change for its callers. Reserve async for functions that genuinely perform asynchronous work.

A quick debugging checklist

When an async bug appears, work through these questions. Did I await every promise I need? Are independent tasks running in parallel? Am I using forEach with an async callback? Does every promise have an error path? Is something returning too early because a return is missing? Most async bugs fall into one of these categories.

The takeaway

Async/await is syntactic sugar over promises, and it hides some of the machinery without removing it. Remember that an async function always returns a promise, that await only pauses the function it is in, and that every promise needs an owner who handles failure. Keep those three ideas in mind and the seven mistakes above will mostly disappear from your code.