Why async/await Is Not Working in JavaScript — Common Mistakes Fixed

async/await looks simple, but there are half a dozen ways it silently breaks. Promises that never resolve, missing awaits, swallowed errors, sequential when it should be parallel — this guide covers every common mistake with before/after fixes.

8+

common async/await bugs covered with fixes

await

missing keyword = silent undefined, not an error

Promise.all

correct pattern for parallel async operations

try/catch

only way to catch errors from async functions

1

Mistake 1 — Missing await

Most common silent bug

Without await, you get back a Promise object, not the resolved value. The function doesn't throw — it silently returns undefined when you access properties on the Promise. This is the most common async/await bug because it fails quietly rather than crashing loudly.

Missing await — returns Promise instead of value

❌ Bad
async function getUser(id) {
  const user = fetch(`/api/users/${id}`); // ❌ missing await — user is a Promise
  console.log(user); // Promise { <pending> } — NOT the user data
  return user.name;  // undefined — accessing .name on a Promise object
}

Await both fetch and .json()

✅ Good
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`); // ✅ await the fetch
  const user = await res.json();                  // ✅ await the json parsing too
  console.log(user); // { id: 1, name: 'Alice' }
  return user.name;  // 'Alice'
}
2

Mistake 2 — await in forEach (Doesn't Work)

forEach is not async-aware

Array.forEach fires all callbacks and returns immediately — it does not wait for async callbacks to complete. The awaits inside forEach are real but they only pause the callback, not the outer function. The "Done" log prints before any emails are sent.

await inside forEach — callbacks not awaited

❌ Bad
// forEach doesn't wait for async callbacks
async function processUsers(users) {
  users.forEach(async (user) => {  // ❌ awaits inside forEach are ignored
    await sendEmail(user.email);
  });
  console.log('Done'); // prints BEFORE emails are sent — wrong!
}

for...of for sequential, Promise.all for parallel

✅ Good
// Option A: for...of (sequential — each waits for previous)
async function processUsers(users) {
  for (const user of users) {
    await sendEmail(user.email);  // ✅ truly sequential, one at a time
  }
  console.log('Done'); // prints after all emails sent

}

// Option B: Promise.all (parallel — all start at once, faster)
async function processUsersFast(users) {
  await Promise.all(users.map(user => sendEmail(user.email)));
  console.log('Done'); // prints after all done in parallel
}
3

Mistake 3 — Sequential Instead of Parallel

Sequential: 900ms total

❌ Bad
// Sequential: each await blocks until the previous finishes
async function loadDashboard() {
  const user    = await getUser();     // 300ms — then waits
  const orders  = await getOrders();   // 400ms — then waits
  const stats   = await getStats();    // 200ms — then waits
  // Total: 900ms 🐢 — even though these are independent requests!
}

Parallel: 400ms — 2.25× faster

✅ Good
// Parallel with Promise.all: all start at once
async function loadDashboard() {
  const [user, orders, stats] = await Promise.all([
    getUser(),    // starts immediately
    getOrders(),  // starts immediately, doesn't wait for getUser
    getStats(),   // starts immediately
  ]);
  // Total: ~400ms (bounded by the slowest request) 🚀
  // 900ms → 400ms just by switching to parallel execution
}
4

Mistake 4 — Unhandled Promise Rejection

No error handling — UnhandledPromiseRejection

❌ Bad
// No try/catch — errors are swallowed silently
async function fetchData() {
  const data = await fetch('/api/might-fail').then(r => r.json());
  return data;
}

fetchData(); // if this throws, nobody catches it
// Node.js: UnhandledPromiseRejection warning, may crash
// Browser: console error, but execution continues with broken state

try/catch inside or .catch() at call site

✅ Good
// Always wrap async code in try/catch
async function fetchData() {
  try {
    const res = await fetch('/api/might-fail');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('fetchData failed:', err);
    return null; // return a default, or re-throw to let caller handle it
  }
}

// Or handle at the call site with .catch():
const data = await fetchData().catch(err => {
  console.error('Dashboard load failed:', err);
  return { defaultState: true };
});
5

Mistake 5 — Top-Level await Without async

await without async — SyntaxError

❌ Bad
// SyntaxError: await is only valid in async functions and modules
function getConfig() {
  const config = await fetch('/config.json').then(r => r.json()); // ❌
  return config;
}
// Error: "await is only valid in async functions, async generators
// and modules"

async function, top-level await (modules), or IIFE

✅ Good
// Option A: Make the function async
async function getConfig() {
  const res = await fetch('/config.json');
  return res.json();
}

// Option B: Top-level await (ES2022 — only in ES modules)
// Requires "type": "module" in package.json or .mjs file extension:
const config = await fetch('/config.json').then(r => r.json());

// Option C: IIFE for CommonJS files
(async () => {
  const config = await fetch('/config.json').then(r => r.json());
  startApp(config);
})();
6

Mistake 6 — async Event Listeners

async event listeners swallow errors

Adding an async function as an event listener means any unhandled error inside it becomes an unhandled promise rejection — it will NOT bubble up to the surrounding try/catch or window.onerror in the way you might expect. Always add try/catch inside the async listener.

Unhandled error in async listener — silently lost

❌ Bad
// Error inside async listener is silently lost — nobody catches it
button.addEventListener('click', async () => {
  const data = await riskyOperation(); // if this throws, nobody knows
  displayData(data); // never runs if riskyOperation throws
});

// The click handler returns a Promise, but event listeners ignore return values

try/catch inside every async event listener

✅ Good
// Wrap the async logic with explicit error handling
button.addEventListener('click', async () => {
  try {
    const data = await riskyOperation();
    displayData(data);
  } catch (err) {
    showErrorMessage(err.message);  // user sees a helpful error
    console.error('Click handler failed:', err);
  }
});
7

Mistake 7 — Promise.all Fails Fast

Promise.all — one failure = all fail

❌ Bad
// If ANY promise rejects, Promise.all rejects immediately
const [a, b, c] = await Promise.all([
  fetchA(), // if this rejects, b and c results are lost forever
  fetchB(),
  fetchC(),
]);
// One failure = lose all results, even the successful ones

Promise.allSettled — handle each result independently

✅ Good
// Promise.allSettled: get ALL results, even if some fail
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${i} succeeded:`, result.value);
  } else {
    console.error(`Request ${i} failed:`, result.reason);
    // Show partial data — render what worked, show error for what didn't
  }
});
8

Mistake 8 — Async Constructor or Return

javascriptAsync constructor anti-pattern and fix
// ❌ Constructors cannot be async — 'new' always returns the instance synchronously
class UserService {
  constructor() {
    this.user = await fetchUser(); // SyntaxError: await in non-async context
  }
}

// ❌ Also broken: returning a value from an async function doesn't work as expected
async function getCount() {
  return 42;  // this works, but...
}
const count = getCount(); // count is a Promise { 42 }, not 42!

// ✅ Fix for constructor: use a static async factory method
class UserService {
  constructor(user) {
    this.user = user; // set synchronously from factory
  }

  static async create() {
    const user = await fetchUser();
    return new UserService(user); // create instance after async work
  }
}

const service = await UserService.create(); // ✅ correct
console.log(service.user); // the fetched user

// ✅ Fix for async return value: always await the call
async function getCount() {
  return 42;
}
const count = await getCount(); // count = 42 ✅
console.log(count); // 42
9

Quick Reference

ItemSituationCorrect Pattern
Multiple async, sequentialfor...of loop with awaitEach waits for previous — use when order matters
Multiple async, parallelPromise.all([...])All start at once — fastest when independent
Parallel, partial failure OKPromise.allSettled([...])All complete, check each result status
First result winsPromise.race([...])Resolves with first settled promise (success or error)
Loop with await insidefor...of (not forEach)forEach ignores async callbacks — use for...of
Error handlingtry/catch inside async function.catch() at call site also works
Async constructorStatic async factory methodconstructor() cannot be async
Top-level await (Node)"type": "module" in package.jsonCommonJS: wrap in (async () => {})()
1

Add console.log before and after each await

If the log before the await prints but not after, the Promise never resolved — you have an infinite pending Promise. Check that the function you're awaiting actually resolves: does it have a return statement? Does it call resolve() if it wraps a callback?

2

Check for missing await with strict TypeScript

Enable TypeScript's @typescript-eslint/no-floating-promises rule. It catches every place you call an async function without awaiting it. This catches the #1 async/await bug (missing await) at compile time instead of at runtime.

3

Verify try/catch scope covers the await

The try/catch must wrap the await statement itself, not just the function call. A try/catch outside a setTimeout or Promise.then chain won't catch errors from inside those callbacks.

4

Check if the function is actually async

If you're awaiting a function that doesn't return a Promise, it still works (await on a non-Promise returns the value immediately) — but if the function throws synchronously, the throw propagates immediately, not as a rejected Promise.

5

Use Promise.race with a timeout to detect hung Promises

If a Promise seems to hang forever, race it against a timeout: await Promise.race([yourPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000))]). This turns an infinite hang into a diagnosable error.

Frequently Asked Questions