Notifications

No notifications

/Phase 3

Async — Promises & async/await

JavaScript is single-threaded. Long operations (network, files, timers) don't block — they run *asynchronously* and notify you when done. The journey: callbacks → Promises → async/await. Today you write almost everything with async/await and only drop down to raw Promises for combinators like Promise.all.

On this page

Detailed Theory

# Promises & async/await

Why async at all

JS runs on one thread. If fetch("/api/users") blocked it, the page would freeze for half a second every request. Instead, the operation is handed to the platform (browser/Node) and a *Promise* is returned immediately. When the result arrives, your callback runs.

A Promise has 3 states

  • pending — work in progress
  • fulfilled — completed with a value
  • rejected — failed with an error
Once settled (fulfilled OR rejected) it never changes again.

Creating one (rare in app code, common in libraries)

const wait = ms => new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
});

const flaky = () => new Promise((resolve, reject) => { Math.random() < 0.5 ? resolve("ok") : reject(new Error("nope")); });

Consuming with .then / .catch / .finally

fetch("/api/users")
    .then(res => res.json())
    .then(users => console.log(users))
    .catch(err => console.error(err))
    .finally(() => hideSpinner());

async/await — the modern way

async functions ALWAYS return a Promise. await pauses inside the function until the Promise settles.
async function loadUsers() {
    try {
        const res   = await fetch("/api/users");
        if (!res.ok) throw new Error(HTTP ${res.status});
        const users = await res.json();
        return users;                  // becomes the resolved value of the Promise
    } catch (err) {
        console.error(err);
        return [];
    } finally {
        hideSpinner();
    }
}

const users = await loadUsers(); // top-level await OK in modules

Sequential vs parallel

// ❌ sequential — total = sum of times
const a = await fetchA();
const b = await fetchB();

// ✅ parallel — total = max of times const [a, b] = await Promise.all([fetchA(), fetchB()]);

Promise combinators

combinatorresolves when…rejects when…
Promise.all([a, b])ALL fulfilled — value = array of resultsANY rejects
Promise.allSettledALL settled — value = array of {status, value/reason}never
Promise.raceFIRST settles (either way)first settles is reject
Promise.anyFIRST FULFILLS — value = that resultALL reject (AggregateError)

Async iteration — for await … of

async function* generate() {
    yield 1; yield 2; yield 3;
}
for await (const n of generate()) console.log(n);

Common pitfalls

  • Forgetting awaitif (fetch("/x")) is always truthy (it's a Promise object). The check is meaningless.
  • forEach ignores async — use for…of if you need awaits in order.
arr.forEach(async x => await save(x));     // ❌ doesn't wait — fires all at once
for (const x of arr) await save(x);         // ✅ sequential
await Promise.all(arr.map(save));           // ✅ parallel, waits for all
  • Mixing then and await — pick one in a function for readability.
  • Errors inside new Promise constructor are tricky — throws in the executor *do* reject, but throws in async callbacks scheduled inside don't. Just use async functions.
  • Unhandled rejections crash Node and log a warning in browsers. Always .catch or wrap awaits in try.

Cancellation — AbortController

fetch and many APIs accept an AbortSignal:
const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);

try { const res = await fetch("/api/slow", { signal: ac.signal }); } catch (e) { if (e.name === "AbortError") console.log("cancelled"); }