Introduction
If you spent anytime writing JavaScript, you've almost certainly beyond into a asynchronous code - code that doesn't run top-to-bottom instantly, but instead waits on something (a server response, a timer, a file read). For years, managers developed this with callbacks, and it worked... untill it didn't.
Enter Promises - a cleaner, more readable, and far more managebale way to handle async operations in JavaScript.
This guide will walk you through everything you need to understand promises from the ground up: what problems they solve, how they work under the hood, and how to use them effectively.
1. What Problem do Promises Solve?
To understand Promises, you first need to feel the pain they eliminated.
The Old Way: Callbacks
A callback is simply a function you pass into another function, to be called once the asynchronous work is done.
// Fetching a user, then their posts, then the comments on the first post
getUser(userId, function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
displayComments(comments, function() {
// We're four levels deep... and this is a simple example
console.log("Done!");
});
});
});
});
This deeply nested structure is affectionately (and painfully) known as "Callback Hell" or the Pyramid of Doom.
The problem with callbacks:
- Hard to read - logic is buried inside nested indentation
- Hard to debug - error handling must be manually wired at every level
- Hard to maintain - adding a new step means restructuring the whole pyramid
- No composability - you can't easily reuse or chain operations
The Promise Key
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => displayComments(comments))
.then(() => console.log("Done!"))
.catch(error => console.error("Something went wrong:", error));
Flat. Linear. Readable. One centralized error handler at the bottom. This is what Promises give you.
2. Promise States: The Three Phases of a Promise
Every Promise exists in exactly one of three states at any given moment:
┌─────────────────────────────────────────────────────────────┐
│ PROMISE LIFECYCLE │
│ │
│ ┌───────────┐ resolve() ┌─────────────┐ │
│ │ │ ─────────────► │ FULFILLED │ │
│ │ PENDING │ └─────────────┘ │
│ │ │ reject() ┌─────────────┐ │
│ │ (initial) │ ─────────────► │ REJECTED │ │
│ └───────────┘ └─────────────┘ │
│ │
│ • Pending → The operation is still in progress │
│ • Fulfilled → The operation completed successfully │
│ • Rejected → The operation failed │
│ │
│ Once settled (fulfilled OR rejected), a Promise │
│ is IMMUTABLE — its state can never change again. │
└─────────────────────────────────────────────────────────────┘
| State | Meaning | Has a value? |
|---|---|---|
| Pending | The async operation is still running | No |
| Fulfilled | The operation succeeded | Yes — the resolved value |
| Rejected | The operation failed | Yes — the reason/error |
Key insight: Once a Promise transitions from
pendingto eitherfulfilledorrejected, it is settled and its state is locked forever. This immutability is what makes Promises predictable and trustworthy.
3. The Basic Promise Lifecycle
Creating a Promise
You create a Promise with the new Promise() constructor, which takes an executor function — a function that receives two arguments: resolve and reject.
const myPromise = new Promise((resolve, reject) => {
// Simulate an async operation (e.g., a network request)
const success = true;
if (success) {
resolve("Here is your data! ✅"); // Fulfills the promise
} else {
reject("Something went wrong ❌"); // Rejects the promise
}
});
Think of the executor as the actual async work being done. When it succeeds, you call resolve(value). When it fails, you call reject(reason).
A Real-World Analogy
Imagine ordering a pizza:
- You place your order — the Promise is pending
- The pizza arrives at your door — the Promise is fulfilled (with the pizza as the value)
- The restaurant calls to say they're closed — the Promise is rejected (with the reason as the error) You don't sit at the restaurant waiting. You go about your day and react when something happens. That's asynchronous programming — and that's Promises.
4. Handling Success and Failure
.then() — Handling Success
The .then() method is called when a Promise fulfills. It receives the resolved value as its argument.
const fetchUser = new Promise((resolve, reject) => {
// Pretending to fetch from a server
setTimeout(() => {
resolve({ id: 1, name: "Alice" });
}, 1000);
});
fetchUser.then(user => {
console.log("Got user:", user.name); // "Got user: Alice"
});
.catch() — Handling Failure
The .catch() method is called when a Promise rejects. It receives the rejection reason (usually an error).
const fetchUser = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("User not found"));
}, 1000);
});
fetchUser.catch(error => {
console.error("Failed:", error.message); // "Failed: User not found"
});
.finally() — Always Runs
The .finally() method runs regardless of whether the Promise fulfilled or rejected — perfect for cleanup tasks like hiding a loading spinner.
fetchUser
.then(user => console.log("User:", user))
.catch(error => console.error("Error:", error))
.finally(() => {
console.log("Request complete — hide the spinner!");
});
5. Promise Chaining: The Real Superpower
Promise chaining is where things get truly powerful. Because .then() always returns a new Promise, you can chain multiple .then() calls in sequence — each one receiving the result of the previous.
How Chaining Works
┌──────────┐ .then() ┌──────────┐ .then() ┌──────────┐
│ Promise │ ──────────► │ Promise │ ──────────► │ Promise │
│ 1 │ │ 2 │ │ 3 │
│(fetch │ │(process │ │(display │
│ data) │ │ data) │ │ result) │
└──────────┘ └──────────┘ └──────────┘
│
┌─────────────────────────┘
│ .catch() handles
▼ errors from any step
┌──────────┐
│ .catch │
│ (errors) │
└──────────┘
A Practical Example
// Each .then() receives the return value of the previous step
fetch("https://api.example.com/users/1")
.then(response => {
// Step 1: Convert the raw response to JSON
return response.json();
})
.then(user => {
// Step 2: Use the user data to fetch their posts
console.log("User found:", user.name);
return fetch(`https://api.example.com/posts?userId=${user.id}`);
})
.then(response => {
// Step 3: Convert posts response to JSON
return response.json();
})
.then(posts => {
// Step 4: Display the posts
console.log("Posts:", posts);
})
.catch(error => {
// One catch handles errors from ANY step above
console.error("Something failed:", error.message);
});
The key rules of chaining:
- Whatever you
returnfrom a.then()becomes the input to the next.then() - If you return a Promise, the chain waits for it to settle before proceeding
- A single .catch() at the end handles errors bubbled up from any step in the chain
Callback Hell vs. Promise Chains: A Side-by-Side
CALLBACK HELL │ PROMISE CHAIN
─────────────────────────────────┼──────────────────────────────────
step1(data, function(r1) { │ step1(data)
if (err) handle(err); │ .then(r1 => step2(r1))
step2(r1, function(r2) { │ .then(r2 => step3(r2))
if (err) handle(err); │ .then(r3 => step4(r3))
step3(r2, function(r3) { │ .catch(err => handle(err));
if (err) handle(err); │
step4(r3, function(r4) { │ ✅ Flat and readable
if (err) handle(err); │ ✅ One error handler
// We're here now │ ✅ Easy to add/remove steps
}); │ ✅ Each step is a clear action
}); │
}); │
}); │
│
❌ Deeply nested │
❌ Error handling repeated │
❌ Hard to add steps │
❌ Reads bottom-to-top │
Quick Summary
| Concept | What it means |
|---|---|
| Promise | An object representing a future value — it will either succeed or fail |
| Pending | The initial state; the async work is still happening |
| Fulfilled | The work succeeded; a resolved value is available |
| Rejected | The work failed; an error reason is available |
.then() |
Runs when the Promise fulfills; receives the resolved value |
.catch() |
Runs when the Promise rejects; receives the error |
.finally() |
Always runs after settled, regardless of outcome |
| Chaining | Linking .then() calls sequentially for multi-step async flows |
What's Next?
Now that you understand the fundamentals of Promises, you're ready to explore:
-
async/await— syntactic sugar built on top of Promises that makes async code look even more like synchronous code -
Promise.all()— run multiple Promises in parallel and wait for all to finish -
Promise.race()— resolve or reject as soon as the first Promise settles -
Promise.allSettled()— wait for all Promises to settle, even if some reject Promises are the foundation of modern JavaScript async programming. Master them here, and everything else — includingasync/await— will click into place naturally.
Top comments (0)