Skip to main content

Command Palette

Search for a command to run...

Making Sense of JavaScript Promises

Callbacks gave us the power to handle time. Promises gave us our sanity back. Here is how JavaScript manages future values.

Published
5 min read
H
CS Graduate | Technical Writing | Software development | 20K+ impressions

If you write JavaScript, you spend a lot of your time waiting. You wait for a database to return a user profile, you wait for an API to send back weather data, or you wait for a user to click a button.

Because JavaScript is single-threaded, it can't afford to literally stop and wait. It has to keep the main thread moving. Historically, we handled this using callbacks—passing a function into another function to be executed later.

But as web applications grew more complex, callbacks revealed a fatal flaw. When you have an asynchronous task that depends on another asynchronous task, which depends on a third, your code begins to nest deeply. You end up with the infamous "Callback Hell" or "Pyramid of Doom."

// The Callback Pyramid
getUser('alice', (userError, user) => {
    if (userError) return handleError(userError);

    getPosts(user.id, (postsError, posts) => {
        if (postsError) return handleError(postsError);

        getComments(posts[0].id, (commentsError, comments) => {
            if (commentsError) return handleError(commentsError);
            console.log(comments);
        });
    });
});

This code is hard to read because it grows horizontally, not vertically. Worse, we have to handle errors at every single step manually.

JavaScript needed a better way to represent a value that doesn't exist yet. It needed the Promise.


What is a Promise?

At its core, a Promise is exactly what it sounds like in plain English: an IOU.

It is a JavaScript object that acts as a placeholder for a future value. When you ask a function to fetch data over the network, it doesn't give you the data immediately. Instead, it instantly returns a Promise object. It says, "I don't have your data right now, but I promise I will let you know when I do."

Because you immediately receive this object, you regain control of your code. You aren't just blindly handing off a callback to a third-party function; you are holding an object that you can attach logic to.

The Promise Lifecycle

To understand how to use a Promise, you have to understand its states. A Promise is basically a state machine that can only be in one of three phases:

  1. Pending: The initial state. The operation has started, but hasn't finished yet. The IOU is in your hand.

  2. Fulfilled (or Resolved): The operation was successful. The Promise now holds the data you asked for.

  3. Rejected: The operation failed. The Promise now holds an error message explaining what went wrong.

Diagram Idea: Promise Lifecycle Flow

[ Pending ] 
    │
    ├── (Success) ──> [ Fulfilled ] ──> Triggers .then()
    │
    └── (Failure) ──> [ Rejected ]  ──> Triggers .catch()

Crucially, once a Promise is fulfilled or rejected, it is considered "settled." It can never change its state again.


Handling Success and Failure

Let’s look at how we actually interact with a Promise. We use two primary methods built into the Promise object: .then() for success, and .catch() for failure.

Imagine we have a function called fetchUser() that returns a Promise.

const userPromise = fetchUser('alice'); 

userPromise
    .then((user) => {
        // This runs only if the promise is Fulfilled
        console.log("Got the user:", user.name);
    })
    .catch((error) => {
        // This runs only if the promise is Rejected
        console.error("Something went wrong:", error);
    });

Notice the readability improvement instantly. We ask for the user. Then, we log the user. Catch any errors. It reads like standard English.

The Magic of Promise Chaining

The true power of Promises isn't just a slightly cleaner syntax for a single task. The real magic is how they handle sequences of tasks.

Every time you call .then() on a Promise, it returns a brand new Promise.

This completely solves the nested callback problem. Instead of indenting our code further and further to the right, we can chain our asynchronous actions flatly, moving downward.

Let's rewrite our messy callback example from the beginning of this post, assuming our functions now return Promises instead of taking callbacks:

getUser('alice')
    .then((user) => {
        // Return a new Promise for the next step
        return getPosts(user.id);
    })
    .then((posts) => {
        // Return another Promise
        return getComments(posts[0].id);
    })
    .then((comments) => {
        console.log("Finally got the comments:", comments);
    })
    .catch((error) => {
        // A single catch handles errors from ANY of the promises above!
        console.error("The chain failed somewhere:", error);
    });

Diagram Idea: Callback vs Promise Comparison

Callbacks:                      Promises:
Task 1                          Task 1
  ↳ Task 2                      ↓
      ↳ Task 3                  Task 2
          ↳ Task 4              ↓
                                Task 3

This is a massive architectural shift.

  1. Vertical growth: The code reads top-to-bottom, mimicking how our brains process logical steps.

  2. Centralized error handling: We no longer need if (error) checks at every step. If getUser fails, JavaScript skips all the .then() blocks and drops straight down to the .catch(). If getPosts fails, same thing. One error handler protects the entire chain.

The Takeaway

Promises didn't change what JavaScript was doing under the hood. The browser is still using the event loop and handling tasks in the background.

What Promises changed was the developer experience. By wrapping an asynchronous operation inside a reliable, predictable object, JavaScript gave us a way to reason about time-delayed data as if it were synchronous.

If you understand how to chain .then() and .catch(), you understand the foundation of modern asynchronous JavaScript. And even better, you are perfectly positioned to understand async/await—the syntactic sugar that makes Promises even easier to read. But that is a post for another day.