async/await made asynchronous JavaScript readable, but it also hides what's really happening, which is how subtle bugs sneak in. Here's the mental model plus the three mistakes I see most often.
await just unwraps a Promise
// a function marked async always returns a Promise
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`); // pause until it resolves
return res.json(); // also awaited implicitly
}
// callers await it too
const user = await getUser("42");Mistake 1: sequential when you meant parallel
Awaiting in a loop runs requests ONE AT A TIME. If they don't depend on each other, that's needless slowness — three 1s requests take 3s instead of 1s.
// ❌ sequential — 3 seconds
for (const id of ids) {
users.push(await getUser(id));
}
// ✅ parallel — ~1 second
const users = await Promise.all(ids.map((id) => getUser(id)));Mistake 2: forgetting await
Forget await and you get a Promise object instead of the value — and any error inside becomes an unhandled rejection that fails silently.
// ❌ user is a Promise, not a user; the save may not have finished
const user = getUser("42");
console.log(user.name); // undefined
// ✅
const user = await getUser("42");Mistake 3: error handling
// wrap awaits that can throw in try/catch
try {
const user = await getUser("42");
return user;
} catch (err) {
// network error, non-2xx if you throw on it, JSON parse failure...
logger.error("failed to load user", err);
throw err; // or return a fallback
}Promise.all rejects as soon as ANY promise rejects. If you want every result regardless of individual failures, use Promise.allSettled and inspect each outcome.
Remember three things: await unwraps a promise and pauses the function; parallelise independent awaits with Promise.all; and every await that can throw needs a try/catch. That covers the vast majority of real async code.