JavaScript Promises
Table of Contents + −
In the previous lesson, we learned how to run code after an async task finishes by passing a callback. Now let’s see how promises give us a cleaner way to handle that same result.
🔮 What is a Promise?
A promise is an object that represents a value that will be ready in the future. When you start an async task, like fetching data from a server, you do not get the value right away. The promise is a placeholder you hold onto now, and it will be filled in once the task succeeds or fails.
A promise lives in one of three states. It starts as pending, then settles into either fulfilled or rejected, and once it settles it never changes again.
| State | Meaning | When it happens |
pending | The task is still running | Right after the promise is created |
fulfilled | The task succeeded and has a value | When the work completes successfully |
rejected | The task failed and has an error | When something goes wrong |
📥 Consuming a Promise
Most of the time you do not build promises yourself. You receive one from a built-in function like fetch and then read its result. You read that result with .then() for success and .catch() for errors.
This example fetches some data and handles both outcomes:
fetch("https://api.example.com/user") .then((response) => { console.log("Got a response:", response.status); }) .catch((error) => { console.log("Something went wrong:", error.message); });Let’s walk through what each line does:
fetch(...)starts the request and immediately returns a pending promise..then((response) => ...)runs when the promise is fulfilled, and it receives the resolved value..catch((error) => ...)runs when the promise is rejected, and it receives the error.
🧹 The finally() Method
The .finally() method runs after the promise settles, whether it was fulfilled or rejected. It is the right place for cleanup that should happen either way, such as hiding a loading spinner.
fetch("https://api.example.com/user") .then((response) => console.log("Success:", response.status)) .catch((error) => console.log("Failed:", error.message)) .finally(() => console.log("Request finished"));Here is how this chain runs:
.then()logs the status only when the request succeeds..catch()logs the error only when the request fails..finally()logs “Request finished” every time, because it runs no matter which of the two paths the promise took.
🪜 Flattening the Callback Problem
In the previous lesson, nesting callbacks inside callbacks pushed the code deeper to the right and made it hard to read. Promises fix this because each .then() can return another promise, letting you write the steps in a flat top-to-bottom chain instead.
Here is the same multi-step flow written as a promise chain:
getUser() .then((user) => getOrders(user.id)) .then((orders) => getDetails(orders[0])) .then((details) => console.log("Order details:", details)) .catch((error) => console.log("Failed somewhere:", error.message));Let’s read the chain from top to bottom:
getUser()returns a promise for the user, and the first.then()passes that user intogetOrders.- The second
.then()receives the orders and passes the first one intogetDetails. - The third
.then()receives the details and logs them. - The single
.catch()at the end handles an error from any step in the chain.
Always return inside .then()
When chaining, return the next promise from inside .then(). The following
.then() then waits for that returned promise instead of running too early.
🛠️ Creating a Promise
You will create promises far less often than you consume them, but it helps to see how one is built. You pass a function to new Promise that receives two functions: call resolve(value) when the work succeeds, and reject(error) when it fails.
This promise resolves with a message after a one second delay:
function wait(ms) { return new Promise((resolve, reject) => { if (ms < 0) { reject(new Error("Time cannot be negative")); } setTimeout(() => resolve("Done waiting!"), ms); });}
wait(1000) .then((message) => console.log(message)) // "Done waiting!" after 1 second .catch((error) => console.log(error.message));Let’s break down the pieces:
new Promise((resolve, reject) => ...)builds the promise and hands you the two functions that settle it.reject(new Error(...))flips the promise to rejected whenmsis negative.setTimeout(() => resolve(...), ms)flips the promise to fulfilled with"Done waiting!"once the delay passes.wait(1000)then consumes the returned promise with.then()and.catch(), just like any other promise.
⚠️ Common Mistakes to Avoid
| Mistake | Problem | Solution |
Forgetting .catch() | Errors are swallowed silently and you never see them | Always add a .catch() at the end of the chain |
Not returning inside .then() | The next .then() runs before the work is done | return the next promise so the chain waits for it |
| Mixing callbacks and promises | The flow becomes confusing and hard to follow | Pick one style and stay with it per task |
Here is the return mistake side by side:
// ❌ The chain does not wait for getOrdersgetUser().then((user) => { getOrders(user.id);});
// ✅ Returning the promise makes the next .then() waitgetUser() .then((user) => getOrders(user.id)) .then((orders) => console.log(orders));Compare the two versions:
- In the first version, the
.then()callsgetOrdersbut never returns it, so the chain moves on without waiting for those orders. - In the second version, the arrow function returns
getOrders(user.id), so the next.then()waits for that promise and receives theordersvalue.
🔧 Try It Yourself!
- Write a
wait(ms)function that returns anew Promiseresolving after a delay withsetTimeout. - Call it and use
.then()to log a message once it resolves. - Add a
.catch()and a.finally()and watch which one runs. - Make
waitrejectwhen the delay is negative, then call it with-100to trigger the.catch(). - Chain two
.then()calls andreturna value from the first to see it arrive in the second.
🧩 What You’ve Learned
- ✅ A promise represents a value that will be ready in the future
- ✅ A promise is
pending, then settles asfulfilledorrejected - ✅
.then()handles success,.catch()handles errors,.finally()runs either way - ✅ Chaining
.then()calls flattens the nested callback problem - ✅ You usually consume promises, but
new Promiselets you create one withresolveandreject
🚀 What’s Next?
Now that you can read and chain promises, we will learn a cleaner syntax for the same thing. Let’s continue to Async Await.