Promises & Microtasks
How Promises manage async state transitions β from pending to settled β and why their callbacks always run before setTimeout.
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.
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.
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.
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.).
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
Any object with a .then() method. Promises interop with thenables β if you return a thenable from .then(), the Promise adopts its state.
Runs promises in parallel. Resolves when ALL fulfill (returns array of results). Rejects as soon as ANY promise rejects.
Like .all() but waits for ALL promises to settle (fulfill or reject). Never rejects β returns array of {status, value/reason} objects.
Resolves or rejects with the FIRST promise that settles. Commonly used for timeout patterns.
A rejected Promise with no .catch() handler. In Node.js, this crashes the process. Always add error handling to promise chains.
1// Promise is a state machine: pending β fulfilled | rejected23const promise = new Promise((resolve, reject) => {4 // Async work here...5 setTimeout(() => resolve("data"), 1000);6});78promise9 .then(data => {10 console.log(data); // "data"11 return transform(data); // returns new Promise12 })13 .then(result => render(result))14 .catch(err => handleError(err)) // catches ANY error above15 .finally(() => hideSpinner()); // runs regardless1617// Promise.all β parallel execution18const [users, posts] = await Promise.all([19 fetch('/api/users'),20 fetch('/api/posts'),21]);2223// Promise.race β first to settle wins24const result = await Promise.race([25 fetch('/api/data'),26 timeout(5000), // reject after 5s27]);
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
1Parallel API Calls with Promise.all()
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.
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.
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()
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.
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.
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.