HT
How Things Work

Promises & Microtasks

How Promises manage async state transitions β€” from pending to settled β€” and why their callbacks always run before setTimeout.

How It Works

A Promise is a state machine representing an asynchronous operation's eventual result. It provides a clean, chainable API (.then/.catch/.finally) that replaces nested callbacks and integrates with the microtask queue for deterministic execution ordering.

1
Promise States (One-Way Transitions)

A Promise starts in 'pending' state. It can transition to 'fulfilled' (resolved with a value) or 'rejected' (rejected with a reason). Once settled, it NEVER changes state again β€” this immutability is a key design feature.

2
.then() Creates New Promises

Every .then() call returns a NEW Promise. If the handler returns a value, the new Promise is fulfilled with that value. If it returns another Promise, the new Promise 'follows' it. If it throws, the new Promise is rejected.

3
Microtask Queue Integration

Promise callbacks (.then, .catch, .finally) are always scheduled as microtasks, not run synchronously. This means they execute after the current synchronous code completes but before any macrotasks (setTimeout, etc.).

4
Error Propagation

Rejections propagate down the chain until they hit a .catch() handler. If no .catch() exists, you get an 'unhandled promise rejection'. .catch(fn) is equivalent to .then(null, fn) β€” it returns a new Promise, so the chain can continue after catching.

Key Concepts

πŸ”„Thenable

Any object with a .then() method. Promises interop with thenables β€” if you return a thenable from .then(), the Promise adopts its state.

⚑Promise.all()

Runs promises in parallel. Resolves when ALL fulfill (returns array of results). Rejects as soon as ANY promise rejects.

πŸ“ŠPromise.allSettled()

Like .all() but waits for ALL promises to settle (fulfill or reject). Never rejects β€” returns array of {status, value/reason} objects.

🏁Promise.race()

Resolves or rejects with the FIRST promise that settles. Commonly used for timeout patterns.

πŸ’₯Unhandled Rejection

A rejected Promise with no .catch() handler. In Node.js, this crashes the process. Always add error handling to promise chains.

Promise Patterns
tsx
1// Promise is a state machine: pending β†’ fulfilled | rejected
2
3const promise = new Promise((resolve, reject) => {
4 // Async work here...
5 setTimeout(() => resolve("data"), 1000);
6});
7
8promise
9 .then(data => {
10 console.log(data); // "data"
11 return transform(data); // returns new Promise
12 })
13 .then(result => render(result))
14 .catch(err => handleError(err)) // catches ANY error above
15 .finally(() => hideSpinner()); // runs regardless
16
17// Promise.all β€” parallel execution
18const [users, posts] = await Promise.all([
19 fetch('/api/users'),
20 fetch('/api/posts'),
21]);
22
23// Promise.race β€” first to settle wins
24const result = await Promise.race([
25 fetch('/api/data'),
26 timeout(5000), // reject after 5s
27]);
πŸ’‘
Why This Matters

Promises are the foundation of modern async JavaScript. async/await is built on top of Promises. fetch(), most Node.js APIs, and every modern library returns Promises. Understanding their state machine, chaining, and error propagation is essential.

Common Pitfalls

⚠Forgetting to return in .then() handlers. Without return, the next .then() receives undefined instead of your transformed value.
⚠Using .then(success, error) vs .then(success).catch(error) β€” the latter catches errors thrown in the success handler too.
⚠Promise.all() fails fast β€” if 1 of 10 promises rejects, you lose all results. Use Promise.allSettled() to get all results regardless.
⚠Creating promises in a loop without awaiting can fire thousands of requests simultaneously. Use for...of with await or batch with Promise.all() in chunks.
Real-World Use Cases

1Parallel API Calls with Promise.all()

Scenario

Your dashboard page needs data from 5 API endpoints. Loading them sequentially takes 5 seconds (1s each). Users see a blank screen for too long.

Problem

Sequential awaits: await fetch(A); await fetch(B); await fetch(C)... means each request waits for the previous one to finish. Total time = sum of all request times.

Solution

Use Promise.all([fetch(A), fetch(B), fetch(C)...]). All requests fire simultaneously. Total time = time of the SLOWEST request (usually ~1-1.5s instead of 5s).

πŸ’‘

Takeaway: Promise.all() is the standard pattern for independent parallel operations. Use it whenever multiple async operations don't depend on each other's results.

2Timeout Pattern with Promise.race()

Scenario

Your API call sometimes takes 30+ seconds due to server issues. Users are stuck on a loading screen with no feedback, eventually abandoning the page.

Problem

fetch() has no built-in timeout. The browser's default timeout is very long (300s in Chrome). There's no way to configure it with standard fetch options.

Solution

Race the fetch against a timeout Promise: Promise.race([fetch(url), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))]). If fetch doesn't resolve in 5 seconds, the timeout Promise rejects first.

πŸ’‘

Takeaway: Promise.race() is the idiomatic way to implement timeouts in JavaScript. Libraries like axios use AbortController, but the race pattern works universally with any Promise-based API.