1. Introduction
A Promise is an object representing the eventual completion (or failure) of an asynchronous operation, and its resulting value. Promises were introduced to solve the readability and error-handling problems of nested callbacks. Instead of passing a callback into a function, an async function now returns a Promise, and you attach handlers to it using .then() and .catch(). This lets async steps be chained in a flat, linear structure instead of nesting deeper and deeper.
Cricket analogy: A Promise is like a bookmaker's slip for a rain-affected match outcome — instead of nesting "if rain stops, then if umpires agree, then if ground is fit" callbacks, you get one slip you attach .then(declareResult) and .catch(abandonMatch) to, chained flatly.
2. Syntax
// Creating a Promise
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve("done");
} else {
reject(new Error("failed"));
}
});
// Consuming a Promise
promise
.then((value) => console.log(value))
.catch((error) => console.error(error))
.finally(() => console.log("cleanup"));
// Combinators
Promise.all([p1, p2, p3]); // resolves when ALL resolve, rejects if any rejects
Promise.race([p1, p2, p3]); // settles as soon as ANY settles
Promise.allSettled([p1, p2, p3]); // waits for all, never rejects3. Explanation
A Promise is always in exactly one of three states: pending (initial, not yet settled), fulfilled (the operation succeeded, with a resulting value), or rejected (the operation failed, with a reason). Once a Promise settles -- becomes fulfilled or rejected -- it is immutable: it cannot change state again, and its value/reason never changes. This immutability is important because it means multiple .then() handlers attached to the same Promise will all see the same final value, no matter when they were attached.
Cricket analogy: A match result is pending during play, then settles to fulfilled (a winner declared) or rejected (abandoned) — once umpires confirm the result it's immutable, so whether a fan checks the scoreboard immediately or an hour later, they see the same final result.
Each call to .then() returns a NEW Promise, which is what makes chaining possible: .then(a).then(b) runs a, waits for its result (even if a returns another Promise), and passes that result into b. If any handler in the chain throws, or the underlying Promise rejects, control jumps forward to the nearest .catch(). This gives Promises a single, predictable error-handling channel, unlike callbacks where each level needs its own check.
Cricket analogy: .then(bowlOver).then(reviewDRS) chains because each .then() hands off a fresh result — bowling the over finishes, its outcome feeds into the DRS review — and if a no-ball is missed or DRS fails, control jumps straight to a single .catch(callThirdUmpire) instead of checks at every stage.
Promise callbacks (the functions passed to .then/.catch/.finally) are scheduled as microtasks, not macrotasks. Microtasks run after the currently executing synchronous code finishes, but BEFORE the event loop moves on to the next macrotask (like a setTimeout callback) -- even a setTimeout with a 0ms delay. This is why a resolved Promise's .then() callback consistently fires earlier than a setTimeout(fn, 0) scheduled around the same time.
4. Example
function getUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetched user");
resolve({ id, name: "Alice" });
}, 100);
});
}
function getOrders(user) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Fetched orders for " + user.name);
resolve(["order1", "order2"]);
}, 100);
});
}
console.log("Start");
getUser(1)
.then((user) => getOrders(user))
.then((orders) => {
console.log("Orders:", orders);
})
.catch((err) => console.error("Error:", err))
.finally(() => console.log("Done"));
console.log("End");5. Output
Start
End
Fetched user
Fetched orders for Alice
Orders: [ 'order1', 'order2' ]
Done6. Key Takeaways
- A Promise has exactly one state at a time: pending, fulfilled, or rejected -- and is immutable once settled.
- .then() always returns a new Promise, enabling flat chaining instead of nested callbacks.
- Errors anywhere in a chain propagate forward to the nearest .catch().
- Promise callbacks run as microtasks, which always run before the next macrotask (e.g. a setTimeout), even with a 0ms delay.
- Promise.all fails fast (rejects if any input rejects); Promise.allSettled always resolves with each result's status.
- An unhandled rejected Promise produces an 'unhandled promise rejection' warning/crash if no .catch() ever handles it.
Practice what you learned
1. Which of these is NOT a valid Promise state?
2. What does .then() return?
3. Given `Promise.resolve(1).then(() => console.log('A')); setTimeout(() => console.log('B'), 0); console.log('C');`, what is the output order?
4. What happens once a Promise has settled (fulfilled or rejected)?
5. How does Promise.all behave if one of the input promises rejects?
Was this page helpful?
You May Also Like
Callbacks and Asynchronous JavaScript
Learn how JavaScript handles operations that take time using callback functions, and why deeply nested callbacks lead to callback hell.
async/await in JavaScript
Learn how async/await provides synchronous-looking syntax for working with Promises, including error handling with try/catch.
The Event Loop in JavaScript
Understand the call stack, macrotask queue, and microtask queue, and master the ordering rules that govern async execution.
Fetch API in JavaScript
Use the Fetch API to make HTTP requests, and learn the critical gotcha that fetch does not reject on HTTP error status codes.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics