JavaScript Promises

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:

promises.js
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.

promises.js
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:

promises.js
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 into getOrders.
  • The second .then() receives the orders and passes the first one into getDetails.
  • 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:

promises.js
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 when ms is 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:

promises.js
// ❌ The chain does not wait for getOrders
getUser().then((user) => {
getOrders(user.id);
});
// ✅ Returning the promise makes the next .then() wait
getUser()
.then((user) => getOrders(user.id))
.then((orders) => console.log(orders));

Compare the two versions:

  • In the first version, the .then() calls getOrders but 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 the orders value.

🔧 Try It Yourself!

  1. Write a wait(ms) function that returns a new Promise resolving after a delay with setTimeout.
  2. Call it and use .then() to log a message once it resolves.
  3. Add a .catch() and a .finally() and watch which one runs.
  4. Make wait reject when the delay is negative, then call it with -100 to trigger the .catch().
  5. Chain two .then() calls and return a 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 as fulfilled or rejected
  • .then() handles success, .catch() handles errors, .finally() runs either way
  • ✅ Chaining .then() calls flattens the nested callback problem
  • ✅ You usually consume promises, but new Promise lets you create one with resolve and reject

🚀 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.

Share & Connect