๐Ÿณ IntuitiveFE
Login
โ† All concepts

Promises & Async/Await

โฑ๏ธ ~5-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿฝ๏ธ A Promise is a catering order slip โ€” you hand the kitchen a request, get a slip back immediately, and attach handlers for when the food arrives (or gets 86'd).

The catering analogy

You walk up to the kitchen window and place an order. The kitchen hands you an order slip (the Promise) immediately โ€” you don't wait at the counter. The slip starts in pending state. When the food is ready the kitchen calls out your number (resolve). If they run out of ingredients, they send back an โ€œ86'dโ€ notice (reject). You pre-attach instructions to the slip: โ€œwhen ready, do Xโ€ (.then), or โ€œif 86'd, do Yโ€ (.catch).

The 3 states โ€” immutable once settled

โณ Pending

Initial state. Mutable โ€” can still transition to fulfilled or rejected.

โœ… Fulfilled

Resolved with a value. Immutable โ€” cannot change again. .then handlers fire.

โŒ Rejected

Rejected with a reason. Immutable. .catch handlers fire.

Once a promise settles (fulfills or rejects), calling resolve() or reject() again is a no-op. The state is permanently locked.

async/await is syntactic sugar โ€” exact transformation

async/await (sugar)

js
1async function getData() {
2 const x = await somePromise();
3 const y = await otherPromise(x);
4 return y;
5}

Desugared to .then()

js
1function getData() {
2 return somePromise()
3 .then(x => otherPromise(x))
4 .then(y => y);
5}

The async keyword wraps the return value in a Promise. Each await desugars to a .then() call, with everything after it as the callback. The suspension mechanics are identical โ€” only the syntax differs.

Promise combinators โ€” choose the right tool

CombinatorFulfills whenRejects when
Promise.all()All fulfillAny single one rejects (fast-fail)
Promise.allSettled()All settle (fulfill or reject)Never โ€” returns all outcomes
Promise.race()First settled (fulfill or reject)If first settled is rejected
Promise.any()First to fulfillAll reject (AggregateError)
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Code

js
1new Promise(resolve => resolve(42))
2 .then(v => console.log(v));

Promise state

โณ Pending

Microtask queue

Queue: empty

Console output

(no output yet)

Steps

Step 1 / 4
The Promise constructor runs synchronously. The executor function is called immediately. The promise state is 'pending' โ€” no value yet.
Gotcha: The .then callback never runs synchronously โ€” even if the promise is already resolved. It is always scheduled as a microtask.
Key insight: resolve() settles the promise synchronously, but .then callbacks are always async (microtask). This is why you can't 'return' out of a promise.
Explored:โœ…โŒโ›“๏ธโณ๐Ÿ”€
๐ŸŽฏ

Challenge

Explore all 5 Promise patterns, stepping through each to the final stage. Pay special attention to async/await ordering โ€” it surprises everyone.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Interview questions

Q: What is Promise chaining and why does each .then return a new Promise?

js
1Promise.resolve(1)
2 .then(x => x + 1) // returns NEW Promise(fulfilled: 2)
3 .then(x => x * 2) // returns NEW Promise(fulfilled: 4)
4 .then(console.log); // 4

Each .then() returns a brand-new Promise whose settled value is whatever the handler returns. This enables chaining because each link in the chain is independent โ€” you can branch, catch errors mid-chain, or return a new Promise from inside a handler (flatMap semantics).

Q: What happens when you forget to return inside .then?

js
1Promise.resolve(1)
2 .then(x => { x + 1; }) // โ† no return! returns undefined
3 .then(x => console.log(x)); // undefined
4ย 
5// Fix:
6Promise.resolve(1)
7 .then(x => x + 1) // implicit return
8 .then(x => console.log(x)); // 2

Missing return is the #1 Promise bug. The next .then receives undefined because the handler implicitly returns undefined. Lint rule: no-floating-promises + consistent-return.

Q: Difference between .catch(fn) and .then(null, fn)?

js
1// These are functionally identical:
2p.catch(fn)
3p.then(null, fn) // .catch is sugar for this
4ย 
5// KEY DIFFERENCE โ€” .catch at end catches errors in .then too:
6p.then(onFulfilled).catch(onRejected)
7// โ†‘ catches errors thrown by onFulfilled too
8ย 
9p.then(onFulfilled, onRejected)
10// โ†‘ onRejected does NOT catch errors thrown by onFulfilled

Prefer .then(onFulfilled).catch(onRejected) โ€” it catches errors from both the original promise AND the fulfillment handler. The two-argument form is a footgun.

Q: async function always returns a Promise โ€” even for plain values. Why?

js
1async function greet() { return "hello"; }
2greet(); // Promise { <fulfilled: "hello"> }
3greet() === "hello"; // false โ€” always a Promise
4ย 
5// Even an error is wrapped:
6async function boom() { throw new Error("!"); }
7boom(); // Promise { <rejected: Error> } โ€” never throws sync

This is by design. An async function is a uniform interface: callers always get a Promise back, whether the function is truly async or just returns a plain value. It prevents โ€œsometimes asyncโ€ APIs which are notoriously hard to consume correctly.

Production bugs

Bug 1: Unhandled promise rejections crashing Node.js

js
1// โœ— CRASH โ€” missing .catch
2async function fetchUser(id) {
3 return db.query(`SELECT * FROM users WHERE id=${id}`);
4}
5fetchUser(null); // Promise rejected but nobody handles it
6ย 
7// Node.js v15+: process exits with code 1
8// Fix A: always .catch() or await in try/catch
9fetchUser(null).catch(console.error);
10ย 
11// Fix B: global handler (safety net, not primary)
12process.on("unhandledRejection", (reason, promise) => {
13 console.error("Unhandled rejection:", reason);
14 // graceful shutdown: drain connections, then exit
15 process.exit(1);
16});

Bug 2: forEach + async/await โ€” forEach doesn't await

js
1// โœ— WRONG โ€” forEach ignores returned Promises
2const ids = [1, 2, 3];
3ids.forEach(async (id) => {
4 await processItem(id); // these run concurrently & unordered!
5});
6console.log("done"); // prints before any processItem finishes
7ย 
8// โœ“ Sequential โ€” for...of respects await
9for (const id of ids) {
10 await processItem(id); // truly sequential
11}
12ย 
13// โœ“ Concurrent but ordered โ€” Promise.all + map
14await Promise.all(ids.map(id => processItem(id)));

Array.forEachwas designed before async/await. It doesn't await the returned Promise from the callback โ€” each iteration fires immediately without waiting. Use for...of for sequential or Promise.all(arr.map(...)) for concurrent.

๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

ES spec: PromiseReactionJob & microtask scheduling

Per ECMA-262 ยง27.2, when a promise is resolved, the engine enqueues a PromiseReactionJob on the microtask queue (called the PromiseJobs queue in the spec). This job, when processed, calls your .then handler with the settled value.

js
1// Simplified spec pseudocode for .then():
2Promise.prototype.then = function(onFulfilled) {
3 const resultPromise = new Promise(...);
4 // EnqueueJob("PromiseJobs", PromiseFulfillReactionJob,
5 // [{ capability: resultPromise, handler: onFulfilled }])
6 return resultPromise;
7};
8ย 
9// Order guarantee:
10setTimeout(() => console.log("macro"), 0);
11Promise.resolve().then(() => console.log("micro"));
12console.log("sync");
13// Output: sync โ†’ micro โ†’ macro
14// Microtasks drain BEFORE the next macrotask

Microtasks (Promise callbacks, queueMicrotask, MutationObserver) always run after the current synchronous code finishes and before the event loop picks up the next macrotask (setTimeout, I/O callbacks).

Promise resolution procedure ยง27.2.1.3 โ€” thenables

When you resolve a promise with a thenable (any object with a .thenmethod), the spec triggers the Promise Resolution Procedure recursively โ€” it doesn't just adopt the value, it follows the chain.

js
1// Returning a Promise from .then():
2Promise.resolve(1)
3 .then(x => Promise.resolve(x + 1)) // โ† returning a Promise
4 .then(x => console.log(x)); // 2 โœ“ (not Promise object)
5ย 
6// The spec calls .then() on the inner promise,
7// adding an EXTRA microtask tick. This matters:
8console.log("a");
9Promise.resolve().then(() => {
10 console.log("b");
11 return Promise.resolve(); // adds extra tick!
12}).then(() => console.log("c"));
13Promise.resolve().then(() => console.log("d"));
14// a โ†’ b โ†’ d โ†’ c (not a โ†’ b โ†’ c โ†’ d)

This โ€œextra tickโ€ when a .then handler returns a native Promise is mandated by the spec (ยง27.2.2.1.3.fโ€“g) and differs from returning a plain value. It trips up ordering assumptions in concurrent code.

async/await desugaring โ€” generators under the hood

In the original TC39 proposal, async functions were specified as generators (function*) wrapped in a driver. Modern engines implement them as direct CPS (continuation-passing style) transforms for performance, but the semantics are identical:

js
1// Generator-based equivalent (conceptual):
2function asyncRunner(genFn) {
3 return new Promise((resolve, reject) => {
4 const gen = genFn();
5 function step(value) {
6 const { done, value: result } = gen.next(value);
7 if (done) { resolve(result); return; }
8 Promise.resolve(result).then(step, reject);
9 }
10 step(undefined);
11 });
12}
13ย 
14// So this:
15async function fn() {
16 const x = await p1();
17 const y = await p2(x);
18 return y;
19}
20ย 
21// Becomes (conceptually):
22const fn = () => asyncRunner(function*() {
23 const x = yield p1();
24 const y = yield p2(x);
25 return y;
26});

Promise.allSettled vs Promise.all โ€” when to use each

js
1// Promise.all โ€” stop on first failure
2const results = await Promise.all([
3 fetchUser(1),
4 fetchUser(2),
5 fetchUser(3),
6]); // throws if any fetchUser throws
7ย 
8// Promise.allSettled โ€” always get all outcomes
9const settled = await Promise.allSettled([
10 fetchUser(1),
11 fetchUser(2),
12 fetchUser(3),
13]);
14// [
15// { status: "fulfilled", value: User },
16// { status: "rejected", reason: Error },
17// { status: "fulfilled", value: User },
18// ]
19const succeeded = settled
20 .filter(r => r.status === "fulfilled")
21 .map(r => r.value);
22const failed = settled
23 .filter(r => r.status === "rejected");

Use Promise.all when all results are required to proceed and one failure should abort the whole operation (e.g. loading dependencies). Use Promise.allSettledwhen you want to handle partial success (e.g. batch operations where some may fail).

Top-level await & async iterators

js
1// Top-level await (ES2022, ESM only):
2// In a .mjs file or "type": "module" package.json:
3const config = await fetch("/config.json").then(r => r.json());
4export default config; // module waits for this before exporting
5ย 
6// Async iterators โ€” for await...of:
7async function* streamLines(url) {
8 const res = await fetch(url);
9 const reader = res.body.getReader();
10 const decoder = new TextDecoder();
11 while (true) {
12 const { done, value } = await reader.read();
13 if (done) break;
14 yield decoder.decode(value);
15 }
16}
17ย 
18for await (const line of streamLines("/api/stream")) {
19 console.log(line); // each chunk as it arrives
20}

for await...of works with any async iterable โ€” an object with [Symbol.asyncIterator]() that returns an iterator of Promises. Node.js streams are async iterables natively since v12.

Patterns to know cold

promisify(fn)

Wrap a Node.js callback-style function into a Promise. node:util.promisify does this.

retry(fn, n)

Recursively await fn(), decrement n on failure. Exponential backoff via delay promise.

timeout(p, ms)

Promise.race([p, new Promise((_, r) => setTimeout(r, ms, new Error('timeout')))])

deferred()

Expose resolve/reject outside the constructor. Pattern: let res, rej; const p = new Promise((r,j)=>{res=r;rej=j})

concurrentLimit(fns, n)

Process a queue with at most N in-flight promises at a time. Use p-limit library in production.

memoizeAsync(fn)

Cache the Promise itself (not just the value) to prevent duplicate in-flight requests for the same key.

๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

The two-arg form doesn't catch errors thrown by `onFulfilled`; `.catch` at the end does.

All async functions always return a Promise โ€” even when the return value is a plain scalar.

`forEach` was designed before async/await โ€” it ignores the Promise returned by async callbacks.

Use `Promise.all` when all results are required; use `Promise.allSettled` when you need all outcomes including partial failures.

Returning a Promise from `.then` causes an extra microtask turn before the next handler runs, which can affect interleaving with other microtasks.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is a 'Promise memory leak' and how does it occur?