Episode 1 — Fundamentals / 1.17 — Asynchronous Programming JavaScript
1.17.c — Understanding Promises
In one sentence: A Promise is an object representing a future value — it starts pending, then settles to fulfilled (with a value) or rejected (with a reason) — and provides
.then(),.catch(),.finally()for composable, chainable async control flow.
Navigation: ← 1.17.b — Callbacks · 1.17.d — Async/Await →
1. What is a Promise?
A Promise is like an IOU — a placeholder for a value that does not exist yet but will (or will fail) in the future.
// Real-world analogy:
// You order coffee. The cashier gives you a receipt (Promise).
// The receipt is "pending" while the barista makes it.
// When ready, the receipt "fulfills" — you get your coffee (value).
// If they run out of beans, the receipt "rejects" — you get an apology (error).
Unlike callbacks, you hold the Promise object and decide when and how to react — no inversion of control.
2. Promise states
A Promise is always in one of three states:
┌──── fulfilled (resolved) ── carries a value
│
pending ────┤
│
└──── rejected ── carries a reason (error)
| State | Meaning | Settled? |
|---|---|---|
| pending | Operation in progress, no result yet | No |
| fulfilled | Operation succeeded, value is available | Yes |
| rejected | Operation failed, reason (error) is available | Yes |
Key rules:
- A Promise settles once. It cannot go from fulfilled to rejected or vice versa.
- Once settled, the value or reason is immutable.
3. Creating a Promise
Use the Promise constructor with an executor function that receives resolve and reject:
const myPromise = new Promise(function (resolve, reject) {
// Async operation here
const success = true;
if (success) {
resolve("Operation completed!"); // → fulfilled with this value
} else {
reject(new Error("Something failed")); // → rejected with this reason
}
});
Real example: wrapping setTimeout in a Promise
function delay(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
delay(2000).then(function () {
console.log("2 seconds have passed");
});
Real example: simulating a network request
function fetchUser(userId) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (userId <= 0) {
reject(new Error("Invalid user ID"));
return;
}
resolve({ id: userId, name: "Alice", email: "alice@example.com" });
}, 1000);
});
}
4. Consuming Promises: .then(), .catch(), .finally()
4.1 .then(onFulfilled, onRejected)
fetchUser(1)
.then(function (user) {
console.log("User:", user.name); // runs if fulfilled
});
.then() can also take a second argument for rejection (rarely used in practice):
fetchUser(-1)
.then(
function (user) { console.log(user); },
function (err) { console.error("Rejected:", err.message); }
);
4.2 .catch(onRejected)
Preferred way to handle errors — equivalent to .then(null, onRejected):
fetchUser(-1)
.then(function (user) {
console.log(user);
})
.catch(function (err) {
console.error("Error:", err.message); // "Error: Invalid user ID"
});
4.3 .finally(onFinally)
Runs regardless of fulfillment or rejection — useful for cleanup:
showLoadingSpinner();
fetchUser(1)
.then(function (user) {
renderUser(user);
})
.catch(function (err) {
showError(err.message);
})
.finally(function () {
hideLoadingSpinner(); // always runs
});
5. Promise chaining
Each .then() returns a new Promise, allowing you to chain operations sequentially:
fetchUser(1)
.then(function (user) {
console.log("Got user:", user.name);
return fetchPosts(user.id); // return a Promise → next .then waits for it
})
.then(function (posts) {
console.log("Got posts:", posts.length);
return fetchComments(posts[0].id); // return another Promise
})
.then(function (comments) {
console.log("Got comments:", comments.length);
})
.catch(function (err) {
console.error("Something failed:", err.message); // catches ANY rejection above
});
Compare to callback hell — the same logic is flat, readable, and has centralized error handling.
How chaining works
What .then() callback returns | What the next .then() receives |
|---|---|
| A value (number, string, object) | That value directly |
| A Promise | The resolved value of that Promise |
| Throws an error | Next .catch() receives the error |
Nothing (undefined) | undefined |
Promise.resolve(10)
.then(function (x) { return x * 2; }) // returns 20
.then(function (x) { return x + 5; }) // returns 25
.then(function (x) { console.log(x); }); // logs 25
6. Promise static methods
6.1 Promise.all(iterable)
Waits for all promises to fulfill. Rejects immediately if any one rejects (fail-fast).
const p1 = fetch("/api/users");
const p2 = fetch("/api/posts");
const p3 = fetch("/api/comments");
Promise.all([p1, p2, p3])
.then(function (responses) {
// responses is an array of all three Response objects
console.log("All fetched:", responses.length); // 3
})
.catch(function (err) {
console.error("At least one failed:", err);
});
6.2 Promise.allSettled(iterable)
Waits for all promises to settle (fulfill or reject). Never short-circuits.
const promises = [
fetch("/api/users"),
fetch("/api/broken-endpoint"), // might reject
fetch("/api/posts"),
];
Promise.allSettled(promises).then(function (results) {
results.forEach(function (result, i) {
if (result.status === "fulfilled") {
console.log("Promise", i, "succeeded:", result.value);
} else {
console.log("Promise", i, "failed:", result.reason);
}
});
});
6.3 Promise.race(iterable)
Settles with the first promise to settle (fulfill or reject).
function timeout(ms) {
return new Promise(function (_, reject) {
setTimeout(function () { reject(new Error("Timeout!")); }, ms);
});
}
Promise.race([
fetch("/api/data"),
timeout(5000)
])
.then(function (response) { console.log("Got data in time"); })
.catch(function (err) { console.error(err.message); }); // "Timeout!" if > 5s
6.4 Promise.any(iterable)
Resolves with the first fulfilled promise. Rejects only if all reject (with AggregateError).
Promise.any([
fetch("https://cdn1.example.com/data.json"),
fetch("https://cdn2.example.com/data.json"),
fetch("https://cdn3.example.com/data.json"),
])
.then(function (fastest) {
console.log("First successful response:", fastest);
})
.catch(function (err) {
console.error("All CDNs failed:", err.errors); // AggregateError
});
Comparison table
| Method | Resolves when | Rejects when | Use case |
|---|---|---|---|
Promise.all | All fulfill | Any rejects (fail-fast) | Parallel tasks that all must succeed |
Promise.allSettled | All settle | Never (always resolves) | Need results of all, regardless of failure |
Promise.race | First settles | First settles (if rejection) | Timeout patterns, fastest response |
Promise.any | First fulfills | All reject | Redundant sources, first success wins |
7. Error propagation in promise chains
Errors propagate down the chain until a .catch() handles them:
fetchUser(1)
.then(function (user) {
throw new Error("Oops!"); // error thrown here
return fetchPosts(user.id);
})
.then(function (posts) {
console.log(posts); // SKIPPED — error is propagating
})
.then(function (comments) {
console.log(comments); // SKIPPED — error is propagating
})
.catch(function (err) {
console.error("Caught:", err.message); // "Caught: Oops!" — caught here
})
.then(function () {
console.log("This runs!"); // chain continues after .catch()
});
Key insight: .catch() itself returns a Promise. If the catch handler does not throw, the chain recovers and subsequent .then() calls run normally.
8. Converting callback-based code to Promises
Wrap the callback API in a new Promise:
// Before: callback-based
function readFileCallback(path, callback) {
fs.readFile(path, "utf8", callback);
}
// After: Promise-based
function readFilePromise(path) {
return new Promise(function (resolve, reject) {
fs.readFile(path, "utf8", function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Usage
readFilePromise("config.json")
.then(function (data) { console.log(data); })
.catch(function (err) { console.error(err); });
Node.js provides a built-in utility for this pattern:
const { promisify } = require("util");
const readFile = promisify(fs.readFile);
readFile("config.json", "utf8")
.then(data => console.log(data));
9. Common mistakes with Promises
Mistake 1: Forgetting to return in .then()
// WRONG — fetchPosts result is lost
fetchUser(1).then(function (user) {
fetchPosts(user.id); // no return!
}).then(function (posts) {
console.log(posts); // undefined — not the posts!
});
// CORRECT
fetchUser(1).then(function (user) {
return fetchPosts(user.id); // return the Promise
}).then(function (posts) {
console.log(posts); // actual posts data
});
Mistake 2: Nesting .then() instead of chaining
// WRONG — recreating callback hell with Promises
fetchUser(1).then(function (user) {
fetchPosts(user.id).then(function (posts) {
fetchComments(posts[0].id).then(function (comments) {
console.log(comments); // nested mess
});
});
});
// CORRECT — flat chain
fetchUser(1)
.then(function (user) { return fetchPosts(user.id); })
.then(function (posts) { return fetchComments(posts[0].id); })
.then(function (comments) { console.log(comments); })
.catch(function (err) { console.error(err); });
Mistake 3: Missing .catch() at the end
// WRONG — unhandled rejection (may crash Node.js or show console warning)
fetchUser(-1).then(function (user) {
console.log(user);
});
// CORRECT
fetchUser(-1)
.then(function (user) { console.log(user); })
.catch(function (err) { console.error("Handled:", err.message); });
10. Quick-reference: Promise.resolve() and Promise.reject()
// Create an already-fulfilled Promise
const resolved = Promise.resolve(42);
resolved.then(function (val) { console.log(val); }); // 42
// Create an already-rejected Promise
const rejected = Promise.reject(new Error("fail"));
rejected.catch(function (err) { console.error(err.message); }); // "fail"
// Useful for starting a chain or testing
Promise.resolve()
.then(function () { return fetchUser(1); })
.then(function (user) { console.log(user); });
Key takeaways
- A Promise represents a future value — pending, fulfilled, or rejected.
- Use
.then()for success,.catch()for errors,.finally()for cleanup. - Chaining keeps async code flat — each
.then()returns a new Promise. Promise.all= all must succeed;Promise.allSettled= get all results;Promise.race= first to settle;Promise.any= first to succeed.- Errors propagate through the chain until caught by
.catch(). - Always return inside
.then()and always add.catch()at the end of a chain.
Explain-It Challenge
Explain without notes:
- What are the three states of a Promise, and can a Promise change state more than once?
- What is the difference between
Promise.allandPromise.allSettled? - Why is nesting
.then()inside another.then()considered an anti-pattern?
Navigation: ← 1.17.b — Callbacks · 1.17.d — Async/Await →