From callbacks to async/await
What a promise actually is, why await only pauses the function it is in, and the try/catch that never catches.
After this lesson you can
- Explain what state a promise is in and how it gets there
- Say why an unhandled rejection is different from a thrown error
- Spot a promise that was never awaited
A promise is an object representing a value that is not ready yet. It is always in exactly one of three states: pending, fulfilled with a value, or rejected with a reason — and once it leaves pending, it never changes state again.
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
wait(100).then(() => console.log("done"));
async/await is syntax over the same promises — it does not replace
them, it makes them readable.
async function load() {
const res = await fetch("/api/users/1");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
An async function always returns a promise, whatever you return
inside it. await pauses that function until the promise settles; it
does not pause the rest of the program, which keeps running.
Try it
function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms));} let running = 0;let peak = 0; async function task() { running++; peak = Math.max(peak, running); await wait(20); running--;} export async function run() { peak = 0; await task(); await task(); await task(); const sequentialPeak = peak; peak = 0; await Promise.all([task(), task(), task()]); const parallelPeak = peak; return { sequentialPeak, parallelPeak };}The try/catch that never catches
try {
sendWelcomeEmail(user.email); // no await
} catch (err) {
logger.warn(err); // never runs
}
try only guards the synchronous part of its block. The call
returns a promise immediately, the block finishes, and the rejection
arrives later with nothing on the stack to catch it — an unhandled
rejection, which under Node's default policy can crash the process.
The fix is await:
try {
await sendWelcomeEmail(user.email);
} catch (err) {
logger.warn(err); // now it actually runs
}
async function sendEmail() { throw new Error("SMTP down");} export async function run() { const log = []; try { const pending = sendEmail(); // no await log.push("try block finished"); pending.catch((e) => log.push(`only .catch saw: ${e.message}`)); } catch (e) { log.push("catch block ran"); // never } try { await sendEmail(); } catch (e) { log.push(`awaited, caught: ${e.message}`); } return log;}The habit worth building: every promise is awaited, returned, or explicitly discarded with a comment saying why it is fire-and-forget. A bare promise-returning call sitting on its own line as a statement always deserves a second look.
Sequential awaits waste the wait
const a = await loadUser(id); // waits
const b = await loadOrders(id); // then waits again, even though
// loadOrders never needed `a`
If two awaited calls do not depend on each other, awaiting them one
after another adds their times together for no reason. Promise.all
starts both immediately and waits once:
const [a, b] = await Promise.all([loadUser(id), loadOrders(id)]);
Promise.all rejects as soon as any one promise rejects, and cancels
nothing — the others keep running in the background. Promise.allSettled
is for when you want every outcome, failures included, rather than the
first failure stopping everything.
Common mistakes
tryaround a call you did notawait. The block finishes before the promise rejects, so thecatchnever runs — the second example shows it.- Awaiting independent calls one after another. The waits add up;
start them together and
await Promise.all(...). The first example counts how many tasks were actually running at once. Promise.allwhen you need every outcome. It rejects on the first failure and the other results are lost to you. Use .
Exercise · Pro
Every result, not just the first failure
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade