One of the most powerful features of Promises is the ability to chain them together. Promise chaining allows you to execute a sequence of asynchronous operations where each step waits for the previous one to complete. This creates flat, readable code that flows from top to bottom — a dramatic improvement over nested callbacks.
A .then() handler can return a value, and that value is passed to the next .then() in the chain.
Promise.resolve(5)
.then(value => {
console.log(value); // 5
return value * 2;
})
.then(value => {
console.log(value); // 10
return value + 3;
})
.then(value => {
console.log(value); // 13
});Promise.resolve(5)creates a fulfilled promise with value5- First
.then()receives5, logs it, and returns10 - Second
.then()receives10, logs it, and returns13 - Third
.then()receives13, logs it
The real power of chaining emerges when each step returns a new Promise. JavaScript automatically waits for the Promise to resolve before passing its value to the next .then().
function fetchUser(userId) {
return new Promise(resolve => {
setTimeout(() => resolve({ id: userId, name: "Alice" }), 500);
});
}
function fetchOrders(userId) {
return new Promise(resolve => {
setTimeout(() => resolve([{ id: 101, total: 50 }]), 500);
});
}
function fetchProducts(orderId) {
return new Promise(resolve => {
setTimeout(() => resolve([{ name: "Laptop", price: 50 }]), 500);
});
}
// Beautiful flat chain!
fetchUser(1)
.then(user => {
console.log("User:", user);
return fetchOrders(user.id);
})
.then(orders => {
console.log("Orders:", orders);
return fetchProducts(orders[0].id);
})
.then(products => {
console.log("Products:", products);
})
.catch(error => {
console.error("Error:", error);
});Errors in a Promise chain automatically propagate down to the nearest .catch(). You don't need error handling at every step.
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchProducts(orders[0].id))
.then(products => console.log(products))
.catch(error => {
// Catches ANY error in the entire chain!
console.error("Something failed:", error);
});Promise.resolve()
.then(() => {
console.log("Step 1");
throw new Error("Error in Step 1!");
})
.then(() => {
console.log("Step 2"); // Skipped!
})
.then(() => {
console.log("Step 3"); // Skipped!
})
.catch(error => {
console.error("Caught:", error.message); // "Error in Step 1!"
})
.then(() => {
console.log("Recovery step"); // Runs after catch!
});Important: After a
.catch(), the chain resumes. The.catch()itself can return a value that flows to the next.then().
You can recover from errors in a .catch() and continue the chain:
fetchUser(999) // User doesn't exist
.catch(error => {
console.warn("User not found, using default");
return { id: 0, name: "Guest" }; // Recovery value
})
.then(user => {
console.log("Proceeding with:", user); // { id: 0, name: "Guest" }
return fetchOrders(user.id);
})
.then(orders => console.log(orders));You can attach multiple .then() handlers to the same promise. They run independently and receive the same value.
const promise = Promise.resolve("Hello");
promise.then(value => console.log("A:", value));
promise.then(value => console.log("B:", value));
promise.then(value => console.log("C:", value));
// Output:
// A: Hello
// B: Hello
// C: HelloThis is different from chaining, where each
.then()receives the return value of the previous one.
fetchUser(1)
.then(user => {
fetchOrders(user.id)
.then(orders => {
fetchProducts(orders[0].id)
.then(products => {
console.log(products);
});
});
});fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchProducts(orders[0].id))
.then(products => console.log(products))
.catch(error => console.error(error));Execute multiple promises simultaneously and wait for all to complete.
const userPromise = fetchUser(1);
const ordersPromise = fetchOrders(1);
const productsPromise = fetchProducts(1);
Promise.all([userPromise, ordersPromise, productsPromise])
.then(([user, orders, products]) => {
console.log("User:", user);
console.log("Orders:", orders);
console.log("Products:", products);
})
.catch(error => {
// Fails if ANY promise rejects
console.error("One failed:", error);
});const urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/comments"
];
Promise.all(urls.map(url => fetch(url)))
.then(responses => Promise.all(responses.map(res => res.json())))
.then(([users, posts, comments]) => {
console.log(`Loaded ${users.length} users`);
console.log(`Loaded ${posts.length} posts`);
console.log(`Loaded ${comments.length} comments`);
});Returns the first promise that settles (fulfills or rejects).
const fast = new Promise(resolve => setTimeout(() => resolve("Fast!"), 100));
const slow = new Promise(resolve => setTimeout(() => resolve("Slow!"), 500));
Promise.race([fast, slow])
.then(winner => console.log(winner)); // "Fast!"function fetchWithTimeout(url, timeoutMs) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timeout")), timeoutMs)
)
]);
}Waits for all promises to settle, regardless of whether they fulfill or reject.
const promises = [
Promise.resolve("success"),
Promise.reject("error"),
Promise.resolve("another success")
];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === "fulfilled") {
console.log("Success:", result.value);
} else {
console.log("Failed:", result.reason);
}
});
});
// Output:
// Success: success
// Failed: error
// Success: another successReturns the first fulfilled promise. Only rejects if ALL promises reject.
const promises = [
Promise.reject("Error 1"),
Promise.resolve("First success!"),
Promise.resolve("Second success!")
];
Promise.any(promises)
.then(firstSuccess => console.log(firstSuccess)) // "First success!"
.catch(error => console.error("All failed:", error));// Total time: 1s + 1s + 1s = 3 seconds
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchProducts(orders[0].id))
.then(products => console.log(products));// Total time: ~1 second (all run simultaneously)
Promise.all([fetchUser(1), fetchOrders(1), fetchProducts(1)])
.then(([user, orders, products]) => {
console.log(user, orders, products);
});const userIds = [1, 2, 3, 4, 5];
// Sequential: one user at a time
userIds.reduce((promise, id) => {
return promise.then(() => fetchUser(id).then(user => console.log(user)));
}, Promise.resolve());
// Or with async/await (covered later):
// for (const id of userIds) {
// const user = await fetchUser(id);
// console.log(user);
// }// ❌ Missing return — the next then() gets undefined
fetchUser(1)
.then(user => {
fetchOrders(user.id); // Not returned!
})
.then(orders => {
console.log(orders); // undefined
});
// ✅ Return the promise
fetchUser(1)
.then(user => {
return fetchOrders(user.id);
})
.then(orders => {
console.log(orders); // Correct!
});
// ✅ Implicit return with arrow
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => console.log(orders));// ❌ Catch swallows the error but doesn't recover
fetchUser(999)
.catch(error => {
console.error(error); // Logs error
// Returns undefined!
})
.then(user => {
console.log(user); // undefined
});
// ✅ Return a recovery value
fetchUser(999)
.catch(error => {
console.error(error);
return { id: 0, name: "Guest" }; // Recovery
})
.then(user => {
console.log(user); // { id: 0, name: "Guest" }
});// ❌ Unhandled rejection
fetchUser(1)
.then(user => {
if (!user.active) {
throw new Error("User inactive");
}
return user;
});
// No catch! Error is silently swallowed.
// ✅ Always catch at the end of a chain
fetchUser(1)
.then(user => {
if (!user.active) {
throw new Error("User inactive");
}
return user;
})
.catch(error => console.error(error));// ❌ This doesn't run in order!
const ids = [1, 2, 3];
Promise.all(ids.map(id => fetchUser(id)))
.then(users => console.log(users));
// Users may arrive in any order depending on network!
// ✅ To preserve order with parallel execution:
const ids = [1, 2, 3];
Promise.all(ids.map(id => fetchUser(id)))
.then(users => console.log(users));
// Actually, Promise.all DOES preserve input order in results!Transform a number through multiple operations using promise chaining.
Promise.resolve(5)
.then(/* multiply by 2 */)
.then(/* add 10 */)
.then(/* subtract 3 */)
.then(result => console.log(result)); // Should be 17Fetch user profile and user settings in parallel, then display both.
function fetchProfile(userId) { /* returns Promise */ }
function fetchSettings(userId) { /* returns Promise */ }
// Fetch both in parallel, then log combined resultWrite a function that retries a promise-returning function up to N times.
function withRetry(fn, maxRetries) {
// Return a promise that retries fn up to maxRetries on failure
}
withRetry(() => fetchUser(1), 3)
.then(user => console.log(user))
.catch(error => console.error("All retries failed"));Implement a waterfall function that passes the result of each promise to the next.
function waterfall(promises) {
// promises is an array of functions that return promises
// Each function receives the result of the previous one
}
waterfall([
() => Promise.resolve(5),
(prev) => Promise.resolve(prev * 2),
(prev) => Promise.resolve(prev + 3)
]).then(result => console.log(result)); // 13Predict and explain the output:
Promise.resolve(1)
.then(x => x + 1)
.then(x => { throw new Error("Oops"); })
.catch(() => 5)
.then(x => x + 1)
.then(x => console.log(x))
.catch(err => console.error(err));- Promise chaining creates readable, flat async code
- Each
.then()receives the return value of the previous.then() - Returning a Promise in
.then()waits for it to resolve - Errors propagate down the chain to the nearest
.catch() .catch()can recover by returning a valuePromise.all()runs promises in parallel and waits for allPromise.race()returns the first to settlePromise.allSettled()waits for all regardless of outcomePromise.any()returns the first success- Always return promises in
.then()handlers - Always add a final
.catch()to handle unexpected errors
Build on promise chaining with:
- Promises In Depth — advanced patterns and edge cases
- Async/Await — the modern syntax for promise-based code
- Converting Callbacks to Promises — modernizing legacy code
Happy coding! 🚀