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
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
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()
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'
}Mistake 2 — await in forEach (Doesn't Work)
forEach is not async-aware
await inside forEach — callbacks not awaited
// 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
// 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
}Mistake 3 — Sequential Instead of Parallel
Sequential: 900ms total
// 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
// 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
}Mistake 4 — Unhandled Promise Rejection
No error handling — UnhandledPromiseRejection
// 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 statetry/catch inside or .catch() at call site
// 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 };
});Mistake 5 — Top-Level await Without async
await without async — SyntaxError
// 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
// 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);
})();Mistake 6 — async Event Listeners
async event listeners swallow errors
Unhandled error in async listener — silently lost
// 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 valuestry/catch inside every async event listener
// 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);
}
});Mistake 7 — Promise.all Fails Fast
Promise.all — one failure = all fail
// 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 onesPromise.allSettled — handle each result independently
// 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
}
});Mistake 8 — Async Constructor or Return
// ❌ 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); // 42Quick Reference
| Item | Situation | Correct Pattern |
|---|---|---|
| Multiple async, sequential | for...of loop with await | Each waits for previous — use when order matters |
| Multiple async, parallel | Promise.all([...]) | All start at once — fastest when independent |
| Parallel, partial failure OK | Promise.allSettled([...]) | All complete, check each result status |
| First result wins | Promise.race([...]) | Resolves with first settled promise (success or error) |
| Loop with await inside | for...of (not forEach) | forEach ignores async callbacks — use for...of |
| Error handling | try/catch inside async function | .catch() at call site also works |
| Async constructor | Static async factory method | constructor() cannot be async |
| Top-level await (Node) | "type": "module" in package.json | CommonJS: wrap in (async () => {})() |
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?
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.
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.
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.
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.