In my post on JavaScript promises I described a promise as an object that represents a result that isn’t ready yet. async and await are syntax that lets you work with those objects without a pile of .then() calls.
The same code, two ways
With promises:
function showUser(id) {
return getUser(id)
.then((user) => render(user))
.catch((err) => showError(err));
}
With async/await:
async function showUser(id) {
try {
const user = await getUser(id);
render(user);
} catch (err) {
showError(err);
}
}
Both do the same thing. The second reads top to bottom like synchronous code.
Two rules to remember
- An
asyncfunction always returns a promise. Whatever youreturngets wrapped, and anything youthrowbecomes a rejection. awaitcan only pause inside anasyncfunction (or at the top level of an ES module). It pauses that function, not the whole program.
Mistake 1: awaiting in a loop when you don’t need to
// Sequential: each request waits for the previous one
for (const id of ids) {
users.push(await getUser(id));
}
// Parallel: all requests start immediately
const users = await Promise.all(ids.map((id) => getUser(id)));
If the calls don’t depend on each other, run them together. On ten requests of 200 ms each, that is about 2 seconds versus about 200 ms.
Mistake 2: forEach doesn’t wait
ids.forEach(async (id) => {
await save(id); // forEach ignores the returned promise
});
console.log("done"); // runs before the saves finish
Use for...of if you need order, or Promise.all with map if you don’t.
When one failure shouldn’t sink everything
Promise.all rejects as soon as any promise rejects. If you want every result regardless, use Promise.allSettled:
const results = await Promise.allSettled(ids.map(getUser)); const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
Takeaway
Think of await as “wait for this promise, then continue”. Use Promise.all when work can overlap, and always wrap awaits in try/catch (or handle the rejection at the call site).
