Warming up the neural circuits...
By the end of this chapter you will:
.then(), .catch(), and .finally() to handle success, failure, and cleanupPromise.allallSettled vs all vs race vs anyasync/await and avoid common sequential bottlenecksPromise.withResolvers() pattern for advanced controlJavaScript is single-threaded — it can only do one thing at a time. If you fetched data from a server synchronously (blocking), the entire page would freeze until the response arrives. No clicks, no animations, nothing.
Promises are JavaScript's solution. They are placeholders for a value that hasn't arrived yet. Think of them like a restaurant buzzer — you hand in your order (start the async operation), receive a promise (the buzzer), and go sit down. The buzzer tells you when your order is ready (the value is resolved) — you weren't blocking the counter the whole time.
// ❌ Hypothetical blocking code (not how JS works, but illustrates the problem)
const data = fetchFromServer(); // Page freezes for 3 seconds
// ✅ Promise-based (real JS) — page stays interactive
const dataPromise = fetchFromServer(); // Returns immediately with a Promise
dataPromise.then(data => render(data1. Pending — The initial . The operation is in progress. Neither succeeded nor failed yet.
2. Fulfilled — The operation completed successfully. The Promise has a value.
3. Rejected — The operation failed. The Promise has a reason (error).
4. Settled — The final state. A Promise is settled once it is either Fulfilled or Rejected. It never changes state after that.
5. Sequential Execution — Running async operations one after another: await A; await B. Total time = time(A) + time(B). Slow when A and B don't depend on each other.
6. Parallel Execution — Running async operations simultaneously: Promise.all([A, B]). Total time = max(time(A), time(B)). Much faster.
React components cannot be async functions (yet). If you fetch data, you must store the result in state. When the Promise settles, call setState — this signals React to re-render the UI with the fresh data. The useEffect is the standard place to start async operations.
new Promise((resolve, reject) => ...) — Creating a PromiseThe Promise constructor takes an executor function that receives two callbacks:
resolve(value) — call this when the operation succeedsreject(reason) — call this when the operation fails// Wrapping a callback-based API (setTimeout) in a Promise
function
| Method | Resolves when | Rejects when | Best for |
|---|---|---|---|
Promise.all | ALL fulfill | ANY rejects | Dependent data (need all or none) |
Promise.allSettled | ALL settle | Never | Independent tasks (log all results) |
Promise.race | First settles | First rejects | Timeouts (network vs. timer) |
Promise.any | First fulfills | ALL reject | Fallbacks (primary vs. mirror) |
| Mistake | What goes wrong | The fix |
|---|---|---|
await inside forEach | forEach ignores returned Promises — doesn't wait | Use for...of or Promise.all(arr.map(...)) |
Forgetting .catch() | Uncaught rejection crashes Node.js processes | Always attach .catch() or use try/catch |
Missing await before a Promise | Returns the Promise object, not the resolved value | Add await keyword |
Sequential await in a loop | time(A) + time(B) + time(C) instead of max(...) | Use |
delay(ms) that returns a Promise resolving after ms milliseconds. It should be usable with await.await delay(1000); console.log("Done!")"Done!" logged after 1 second/api/user and /api/settings at the same time (not one-by-one). Return both results as { user, settings }.{ user: {...}, settings: {...} }withTimeout(promise, ms) function that rejects with "Timed out" if the given promise doesn't resolve within ms milliseconds.withTimeout(slowFetch(), 2000)"Timed out" if slowFetch takes more than 2 secondsretry(fn, times) function that calls fn() up to times attempts, only failing if all attempts fail.retry(() => unstableFetch(), 3)Promise.withResolvers() to create a confirm(message) function that returns a Promise which resolves true when the user clicks OK and false when they click Cancel. (Assume you can create DOM elements.)const ok = await confirm("Delete file?")true or false based on button clickedWhat does Promise.all() do?
Real-world: Wrapping a file reader
function readFileAsText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(new Error("File read failed"));
reader.readAsText(file);
});
}.then(onFulfilled) — The success handlerSchedules a callback to run when the Promise is fulfilled. Returns a new Promise — this is what enables chaining.
fetch("/api/user/1")
.then(response => response.json()) // Transform Response → JS object
.then(user => user.name.toUpperCase()) // Transform data
.then(name => console.log(name)); // Use the final resultEach .then() receives the return value of the previous .then(). If you return a Promise inside .then(), the chain waits for it to settle before continuing.
| Returns | Use when |
|---|---|
| New Promise | Handling or transforming a fulfilled value |
.catch(onRejected) — The safety netSchedules a callback for any rejection in the chain above it. One .catch() at the end handles all upstream errors.
fetch("/api/user/1")
.then(res => res.json())
.then(user => updateUI(user))
.catch(error => {
// Catches network errors, JSON parse errors, and updateUI errors
showErrorMessage(error.message);
});An unhandled Promise rejection will throw a warning in the browser and crash a Node.js process in newer versions. Always attach a .catch() or wrap with try/catch.
.finally(callback) — Always runsRuns whether the Promise fulfilled or rejected. Perfect for hiding loading spinners or releasing locks.
setLoading(true);
fetch("/api/data")
.then(res => res.json())
.then(data => setData(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false)); // Always hides spinner| Feature | .then() | .catch() | .finally() |
|---|---|---|---|
| Runs on success? | ✅ Yes | ❌ No | ✅ Yes |
| Runs on failure? | ❌ No | ✅ Yes | ✅ Yes |
| Receives value? | ✅ Yes | ✅ (error) | ❌ No |
| Returns new Promise? | ✅ Yes | ✅ Yes | ✅ Yes |
Promise.all([...promises]) — All or nothingRuns all Promises in parallel. Resolves when every Promise fulfills. Rejects immediately if any single one rejects.
const [user, posts, comments] = await Promise.all([
fetch("/api/user/1").then(r => r.json()),
fetch("/api/user/1/posts").then(r => r.json()),
fetch("/api/user/1/comments").then(r => r.json())
]);
// All three fetches ran simultaneously! Total time ≈ slowest individual fetchWhen NOT to use Promise.all:
allSettled)await)| Resolves when | Rejects when | Best for |
|---|---|---|
| ALL fulfill | ANY one rejects | Dependent data — you need everything or nothing |
Promise.allSettled([...promises]) — Resilient batchRuns all in parallel. Never rejects. Always returns an array of result objects describing each outcome.
const results = await Promise.allSettled([
sendEmail(user),
sendPushNotification(user),
updateActivityLog(user)
]);
// Check what succeeded and what failed
for (const result of results) {
Each result object:
{ status: "fulfilled", value: ... } — on success{ status: "rejected", reason: Error } — on failure| Resolves when | Rejects when | Best for |
|---|---|---|
| ALL settle (either way) | Never | Independent tasks — log all results |
Promise.race([...promises]) — First to settle winsReturns the result of the first Promise to settle — whether fulfilled or rejected.
// Classic timeout pattern
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timed out")),
| Resolves/Rejects when | Best for |
|---|---|
| First to settle (win OR fail) | Timeouts, fastest-response routing |
Promise.any([...promises]) — First SUCCESS winsReturns the result of the first Promise that fulfills. Only rejects if all Promises reject.
// Try multiple CDNs — use whichever responds first successfully
const asset = await Promise.any([
fetch("https://cdn1.example.com/logo.png"),
fetch("https://cdn2.example.com/logo.png"),
fetch("https://cdn3.example.com/logo.png")
]);
// Gets the fastest successful response| Resolves when | Rejects when | Best for |
|---|---|---|
| FIRST fulfills | ALL reject | Fallbacks, mirrored servers |
Decision tree — which batching method?
I have multiple Promises to run...
├── I need ALL results, fail if any fails → Promise.all
├── I need all results regardless of errors → Promise.allSettled
├── I want the first result (win OR fail) → Promise.race
└── I want the first SUCCESS only → Promise.anyasync keywordPlacing async before a function declaration does two things:
await keyword inside the functionasync function getGreeting() {
return "Hello!"; // Automatically wrapped in Promise.resolve("Hello!")
}
getGreeting(); // Promise { "Hello!" } — NOT just the string
await getGreeting(); // "Hello!" — now you get the valueawait keywordawait pauses the execution of the async function until the Promise settles, then returns the resolved value. The of your app keeps running — only this function pauses.
async function loadDashboard(userId) {
const user = await fetchUser(userId); // Pause until user arrives
const posts = await fetchPosts(user.id); // Pause until posts arrive
return { user
try/catchasync function loadUser(id) {
try {
const res = await fetch(`/api/user/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status
// ❌ SLOW: Each await waits for the previous to finish
// Total time = 1s + 1s + 1s = 3 seconds
async function loadSlow() {
const user = await fetchUser(1); // Wait 1s
const posts = await fetchPosts(1); // Wait 1s more
const friends
When should you use sequential await?
When each request depends on the result of the previous one:
async function placeOrder(cartId) {
const cart = await fetchCart(cartId); // Need cart first
const order = await createOrder(cart); // Need cart to create order
const payment = await processPayment(
Promise.withResolvers() — External control (ES2024)The classic Promise constructor forces you to resolve/reject from inside the executor. Sometimes you need to resolve from a completely different part of your code.
The old way (verbose):
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// Now resolve and reject are accessible outside...The new way — Promise.withResolvers():
const { promise, resolve, reject } = Promise.withResolvers();
// You can now call resolve() or reject() from anywhere!
document.querySelector("#confirm").onclick = () => resolve(true);
document.
Real-world: Modal dialog as a Promise
function showConfirmDialog(message) {
const { promise, resolve } = Promise.withResolvers();
const modal = createModal({
message,
onConfirm: () => resolve(true),
onCancel: ()
Promise.resolve(value) and Promise.reject(reason)Create already-settled Promises immediately. Useful for testing and ensuring consistent return types.
// Return a Promise even from a cached (synchronous) result
function getUser(id) {
if (cache.has(id)) {
return Promise.resolve(cache.get(id)); // Sync value wrapped in Promise
}
return fetchUser
Components can't be async, so use useEffect + state:
function UserDashboard({ userId }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true; // Prevent state update after unmount
Promise.all([
fetch(`/api/user/${userId}`).then(r => r.json()),
fetch(`/api/user/${userId}/posts`).then(r => r.json())
])
.then(([user, posts]) => {
if (isMounted) setData({ user, posts });
})
.catch(err => {
if (isMounted) setError(err.message);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => { isMounted = false; };
}, [userId]);
if (loading) return <Spinner />;
if (error) return <ErrorBanner msg={error} />;
return <Dashboard data={data} />;
}Nested .then() inside .then() | Re-creates callback hell | Return the inner Promise to flatten the chain |
Promise.all on 1000+ requests | Overloads network and DB connections | Use a concurrency limiter (p-limit library) |