Why Promises Matter
JavaScript is single-threaded. It can only do one thing at a time. But web apps constantly deal with things that take time — network requests, file reads, timers. Promises are how JavaScript handles this without freezing the browser.
Before promises, we used callbacks. They worked, but they got ugly fast:
getUser(userId, function (user) {
getOrders(user.id, function (orders) {
getOrderDetails(orders[0].id, function (details) {
console.log(details);
});
});
});This is callback hell — deeply nested, hard to read, and painful to debug. Promises fix this.
What Is a Promise?
A Promise is an object that represents a value that may not exist yet. It's a container for a future result.
A promise is always in one of three states:
| State | Meaning |
|---|---|
| pending | Still waiting. No result yet. |
| fulfilled | The operation succeeded. We have a value. |
| rejected | The operation failed. We have an error. |
Once a promise moves from pending to fulfilled or rejected, it is settled and its state never changes again.
const promise = new Promise((resolve, reject) => {
// resolve(value) → moves to fulfilled
// reject(error) → moves to rejected
});The function you pass to new Promise() is called the executor. It runs immediately and receives two callbacks:
resolve(value)— call this when the operation succeedsreject(error)— call this when it fails
Creating Promises
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "Alice" });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}Consuming Promises: .then(), .catch(), .finally()
.then(onFulfilled, onRejected)
Attaches handlers for when the promise settles. Returns a new promise, which is what makes chaining possible.
fetchUserData(1)
.then((user) => {
console.log(user.name); // "Alice"
return user.id;
})
.then((id) => {
console.log("User ID:", id); // "User ID: 1"
});.catch(onRejected)
Shorthand for .then(undefined, onRejected). Catches any error in the chain above it.
fetchUserData(-1)
.then((user) => console.log(user))
.catch((err) => console.error(err.message)); // "Invalid user ID".finally(onFinally)
Runs whether the promise fulfilled or rejected. Great for cleanup. It does not receive any arguments and passes through the original value or error.
fetchUserData(1)
.then((user) => console.log(user))
.catch((err) => console.error(err))
.finally(() => console.log("Done loading"));Promise Chaining
Every .then() returns a new promise. This is the key insight. You can chain operations sequentially:
fetch("/api/user/1")
.then((response) => response.json())
.then((user) => fetch(`/api/orders/${user.id}`))
.then((response) => response.json())
.then((orders) => console.log(orders))
.catch((err) => console.error("Something failed:", err));Compare this to callback hell — flat, readable, and a single .catch() handles errors from any step.
Important rule: If you return a promise inside .then(), the next .then() waits for that promise to settle.
Static Methods You Must Know
Promise.resolve(value) and Promise.reject(error)
Create already-settled promises. Useful for starting chains or testing.
Promise.resolve(42).then((v) => console.log(v)); // 42
Promise.reject(new Error("fail")).catch((e) => console.error(e.message)); // "fail"Promise.all(iterable)
Takes an array of promises. Returns a single promise that fulfills when all of them fulfill, with an array of their values. If any one rejects, the whole thing rejects immediately.
const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");
Promise.all([p1, p2, p3])
.then(([users, posts, comments]) => {
console.log("All loaded!");
})
.catch((err) => {
console.error("One failed:", err);
});Use case: Loading multiple independent resources in parallel.
Promise.allSettled(iterable)
Like Promise.all, but it never short-circuits. Waits for every promise to settle and gives you the outcome of each:
Promise.allSettled([
Promise.resolve("ok"),
Promise.reject("fail"),
Promise.resolve("also ok"),
]).then((results) => {
console.log(results);
// [
// { status: "fulfilled", value: "ok" },
// { status: "rejected", reason: "fail" },
// { status: "fulfilled", value: "also ok" }
// ]
});Use case: When you want results from all operations regardless of failures.
Promise.race(iterable)
Returns a promise that settles as soon as the first promise settles (fulfilled or rejected).
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), 5000)
);
Promise.race([fetch("/api/data"), timeout])
.then((response) => console.log("Got data in time"))
.catch((err) => console.error(err.message)); // "Timeout" if fetch takes > 5sUse case: Setting timeouts on async operations.
Promise.any(iterable)
Returns the first fulfilled promise. Ignores rejections unless all reject, in which case it rejects with an AggregateError.
Promise.any([
fetch("https://cdn1.example.com/data"),
fetch("https://cdn2.example.com/data"),
fetch("https://cdn3.example.com/data"),
]).then((fastest) => console.log("Fastest CDN responded"));Use case: Trying multiple sources and using whichever responds first.
Async/Await — Syntactic Sugar Over Promises
async/await is not a replacement for promises — it's built on top of them.
async function loadUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user;
} catch (err) {
console.error("Failed to load user:", err);
throw err;
}
}asyncbefore a function makes it always return a promiseawaitpauses execution until the promise settles- Use
try/catchfor error handling instead of.catch()
Common Mistake: Sequential When You Want Parallel
// BAD — sequential, slow
async function loadData() {
const users = await fetch("/api/users");
const posts = await fetch("/api/posts"); // waits for users to finish first!
}
// GOOD — parallel, fast
async function loadData() {
const [users, posts] = await Promise.all([
fetch("/api/users"),
fetch("/api/posts"),
]);
}Microtasks and the Event Loop
Promise callbacks (.then, .catch, .finally) are scheduled as microtasks. Microtasks run after the current task but before the next macrotask (like setTimeout).
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2Why?
"1"— synchronous, runs immediatelysetTimeoutcallback — queued as a macrotask.then()callback — queued as a microtask"4"— synchronous, runs immediately- Microtask queue runs →
"3" - Macrotask queue runs →
"2"
Interview tip: If they ask about event loop ordering, remember: sync code → microtasks → macrotasks.
Common Interview Questions
1. What's the difference between Promise.all and Promise.allSettled?
Promise.all short-circuits on the first rejection. Promise.allSettled waits for every promise and reports both successes and failures.
2. What happens if you resolve a promise with another promise?
The outer promise adopts the state of the inner promise. This is called "unwrapping."
const inner = new Promise((resolve) =>
setTimeout(() => resolve("inner value"), 1000)
);
const outer = Promise.resolve(inner);
outer.then((val) => console.log(val)); // "inner value" (after 1 second)3. Can a promise be both fulfilled and rejected?
No. Once settled, a promise's state is immutable. Calling resolve() after reject() (or vice versa) has no effect.
const p = new Promise((resolve, reject) => {
resolve("first");
reject("second"); // ignored
resolve("third"); // also ignored
});
p.then((v) => console.log(v)); // "first"4. What's the output?
Promise.resolve(1)
.then((x) => x + 1)
.then((x) => {
throw new Error("oops");
})
.then((x) => console.log(x))
.catch((e) => console.log(e.message))
.then((x) => console.log("after catch:", x));Answer: "oops" then "after catch: undefined". The error skips the third .then and goes to .catch. After .catch handles the error, the chain continues normally (.catch returns a fulfilled promise with undefined).
Implement Your Own: Promise.all
This is a classic interview question. Let's build it step by step.
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = [];
let completed = 0;
const promiseArray = Array.from(promises);
if (promiseArray.length === 0) {
resolve(results);
return;
}
promiseArray.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = value;
completed++;
if (completed === promiseArray.length) {
resolve(results);
}
},
(error) => {
reject(error);
}
);
});
});
}Key details interviewers look for:
- Wrapping each item with
Promise.resolve()to handle non-promise values - Using
indexto maintain order (not push, which would lose ordering) - Counting completions instead of checking
results.length(sparse arrays are tricky) - Handling the empty array edge case
- Rejecting on the first error
Let's verify it works:
// Test 1: All fulfill
promiseAll([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
.then((r) => console.log(r)); // [1, 2, 3]
// Test 2: One rejects
promiseAll([Promise.resolve(1), Promise.reject("err"), Promise.resolve(3)])
.catch((e) => console.log(e)); // "err"
// Test 3: Non-promise values
promiseAll([1, 2, 3])
.then((r) => console.log(r)); // [1, 2, 3]
// Test 4: Empty array
promiseAll([])
.then((r) => console.log(r)); // []Implement Your Own: Promise.race
function promiseRace(promises) {
return new Promise((resolve, reject) => {
const promiseArray = Array.from(promises);
promiseArray.forEach((promise) => {
Promise.resolve(promise).then(resolve, reject);
});
});
}This one is deceptively simple. The first promise to settle calls resolve or reject, and since a promise can only settle once, all subsequent calls are ignored.
Implement Your Own: Promise.allSettled
function promiseAllSettled(promises) {
return new Promise((resolve) => {
const results = [];
let completed = 0;
const promiseArray = Array.from(promises);
if (promiseArray.length === 0) {
resolve(results);
return;
}
promiseArray.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
results[index] = { status: "fulfilled", value };
completed++;
if (completed === promiseArray.length) {
resolve(results);
}
},
(reason) => {
results[index] = { status: "rejected", reason };
completed++;
if (completed === promiseArray.length) {
resolve(results);
}
}
);
});
});
}Notice how it never rejects. Both fulfillments and rejections are recorded as results.
Implement Your Own: Promise.any
function promiseAny(promises) {
return new Promise((resolve, reject) => {
const errors = [];
let rejectedCount = 0;
const promiseArray = Array.from(promises);
if (promiseArray.length === 0) {
reject(new AggregateError([], "All promises were rejected"));
return;
}
promiseArray.forEach((promise, index) => {
Promise.resolve(promise).then(
(value) => {
resolve(value);
},
(error) => {
errors[index] = error;
rejectedCount++;
if (rejectedCount === promiseArray.length) {
reject(new AggregateError(errors, "All promises were rejected"));
}
}
);
});
});
}Promise.any is the opposite of Promise.all — it resolves on the first success and only rejects when everything fails.
Implement Your Own: Promisify
Node.js has util.promisify. Here's how to build it. This converts callback-style functions into promise-returning functions.
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Usage
const readFile = promisify(fs.readFile);
readFile("./data.txt", "utf8")
.then((content) => console.log(content))
.catch((err) => console.error(err));Implement Your Own: Sequential Promise Execution
Run an array of promise-returning functions one after another:
function promiseSequence(functions) {
return functions.reduce(
(chain, fn) => chain.then((results) =>
fn().then((result) => [...results, result])
),
Promise.resolve([])
);
}
// Usage
const tasks = [
() => fetch("/api/step1").then((r) => r.json()),
() => fetch("/api/step2").then((r) => r.json()),
() => fetch("/api/step3").then((r) => r.json()),
];
promiseSequence(tasks).then((results) => console.log(results));Implement Your Own: Promise with Timeout
Wraps any promise with a timeout:
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
withTimeout(fetch("/api/slow-endpoint"), 3000)
.then((response) => console.log("Got response"))
.catch((err) => console.error(err.message)); // "Timed out after 3000ms"Implement Your Own: Retry with Backoff
Retry a failed async operation with exponential backoff:
function retry(fn, retries = 3, delay = 1000) {
return fn().catch((err) => {
if (retries <= 0) throw err;
return new Promise((resolve) =>
setTimeout(() => resolve(retry(fn, retries - 1, delay * 2)), delay)
);
});
}
// Usage
retry(() => fetch("/api/flaky-endpoint"), 3, 1000)
.then((response) => console.log("Success"))
.catch((err) => console.error("Failed after all retries:", err));Implement Your Own: Promise-Based Throttle
Limit concurrent promise executions:
function throttlePromises(functions, limit) {
return new Promise((resolve, reject) => {
const results = [];
let running = 0;
let index = 0;
let completed = 0;
function runNext() {
if (completed === functions.length) {
resolve(results);
return;
}
while (running < limit && index < functions.length) {
const currentIndex = index++;
running++;
Promise.resolve(functions[currentIndex]())
.then((value) => {
results[currentIndex] = value;
})
.catch((err) => {
results[currentIndex] = err;
})
.finally(() => {
running--;
completed++;
runNext();
});
}
}
runNext();
});
}
// Usage — max 2 concurrent requests
const tasks = urls.map((url) => () => fetch(url));
throttlePromises(tasks, 2).then((results) => console.log(results));Quick Reference Cheat Sheet
| Method | Resolves When | Rejects When |
|---|---|---|
Promise.all | All fulfill | Any one rejects |
Promise.allSettled | All settle | Never |
Promise.race | First settles (either way) | First settles with rejection |
Promise.any | First fulfills | All reject |
Summary
You now know:
- What promises are and their three states
- How to create, chain, and handle errors with
.then(),.catch(),.finally() - All four static methods and when to use each
- How async/await works on top of promises
- Microtask ordering in the event loop
- How to implement
Promise.all,Promise.race,Promise.allSettled,Promise.any,promisify, sequential execution, timeout, retry, and throttle from scratch
That last point is what separates you in interviews. Anyone can call Promise.all. Building it proves you actually understand it.
You might also like
The DOM API — A Beginner-Friendly Guide
Learn how the browser turns HTML into objects you can control with JavaScript. By the end, you'll build a working todo app from scratch.
BlogJavascript Fatigue and Frontend Systems
Navigating the ever-changing landscape of JavaScript frameworks and building frontend systems that last.
FrontendBuild Connect Four from Scratch with Vanilla JS
A complete guide to building Connect Four using only HTML, CSS, and vanilla JavaScript. Covers gravity-based piece dropping, win detection across rows/columns/diagonals, and AI opponent. Interview-ready implementation.