Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises Explained

Published
5 min read
JavaScript Promises Explained
S

Building Web & GenAI Software that (usually) work | Son of proud parents.

Imagine you walk into a busy fast-food restaurant and order a burger. The cashier takes your money, but they don't hand you a burger immediately. Instead, they give you a receipt with an order number.

That receipt is not your food, but it is a promise that you will get your food in the future. While you wait, you don't have to freeze in place; you can go fill your drink, find a table, or talk to your friends. Eventually, your number is called, and you either get your delicious burger, or the cashier apologizes and says they ran out of ingredients.

In JavaScript, we deal with things that take time, like fetching user data from a server or downloading an image. To handle these time-consuming tasks without freezing the entire website, JavaScript uses exactly this concept: Promises.

In this blog, we will understand what problem promises solve, what a promise actually is, the three states of a promise's lifecycle, how to handle success and failure, and how promise chaining makes our code incredibly easy to read.

First, let’s understand the mess we used to live in before Promises existed.

The Problem Promises Solve

Before promises were introduced, JavaScript handled time-consuming tasks using callbacks. A callback is simply a function passed into another function, designed to run only when the task is finished.

For one simple task, a callback is fine. But what if you have a sequence of tasks that depend on each other?

  1. Fetch the user's profile.

  2. Using that profile, fetch their recent posts.

  3. Using the posts, fetch the comments.

Using callbacks, your code would look like this:

getUserProfile(function(profile) {
    getRecentPosts(profile.id, function(posts) {
        getComments(posts[0].id, function(comments) {
            displayComments(comments);
        });
    });
});

Developers affectionately call this "Callback Hell" or the "Pyramid of Doom." The code keeps moving further and further to the right. It becomes incredibly difficult to read, nearly impossible to debug, and handling errors (like a network failure at step 2) turns into an absolute nightmare.

Promises were created to solve this exact problem. They flatten out the code, vastly improving readability and making error handling a breeze.

What is a Promise? (The Lifecycle and States)

Conceptually, a Promise in JavaScript is exactly like your restaurant receipt. It is an object that represents a future value, a value that is not available right now, but will be resolved at some point later.

Every promise goes through a strict lifecycle and will always be in one of these three states:

  1. Pending: This is the initial state. The order has been placed, but the burger is not ready yet. The promise is waiting for the background task to finish.

  2. Fulfilled (Resolved): The task completed successfully. Your number was called, and you got your burger. The promise now holds the final data you requested.

  3. Rejected: The task failed. The restaurant ran out of ingredients, or your internet disconnected. The promise now holds an error message explaining what went wrong.

Once a promise becomes Fulfilled or Rejected, we say it is "settled." A settled promise can never change its state again.

Handling Success and Failure

When a JavaScript function gives you a promise, you need a way to say: "When this is finally ready, do this. If it fails, do that."

We handle this using two built-in methods: .then() and .catch().

  • .then() runs only if the promise is Fulfilled.

  • .catch() runs only if the promise is Rejected.

Let's look at how we would handle our restaurant order in code:

orderBurger()
    .then(function(myBurger) {
        // This runs if the promise is FULFILLED
        console.log("Yum! I am eating my " + myBurger);
    })
    .catch(function(errorMsg) {
        // This runs if the promise is REJECTED
        console.log("Oh no: " + errorMsg);
    });

It reads almost exactly like plain English. Order the burger, then eat it, or catch the error. You don't have to pass complex functions deep inside other functions anymore.

The Promise Chaining Concept

The true superpower of promises is Chaining.

Remember the dreadful Callback Hell we looked at earlier? Because every .then() method actually returns a brand new promise, we can chain multiple .then() blocks together. This completely eliminates the Pyramid of Doom.

Let's rewrite that ugly callback code using promise chaining:

getUserProfile()
    .then(function(profile) {
        return getRecentPosts(profile.id);
    })
    .then(function(posts) {
        return getComments(posts[0].id);
    })
    .then(function(comments) {
        displayComments(comments);
    })
    .catch(function(error) {
        console.log("Something went wrong at one of the steps: " + error);
    });

Look at how beautifully this code flows! Instead of moving diagonally to the right, it reads perfectly from top to bottom.

Even better, we only need one single .catch() at the very bottom. If getUserProfile fails, or getRecentPosts fails, or getComments fails, JavaScript will automatically skip the remaining .then() blocks and jump straight down to the .catch(). This makes error handling incredibly clean and centralized.

Conclusion

Promises modernized the way developers write JavaScript. By treating asynchronous operations as a "future value" rather than a messy web of callbacks, our code becomes much more predictable.

Understanding the three states, pending, fulfilled, and rejected, gives you a mental model for how data flows over time. And by mastering .then() and .catch() chains, you can write complex, multi-step network requests that are as easy to read as a simple list of instructions.

The next time you write a network request, remember the restaurant receipt: you are just waiting for a promise to settle!