DEV Community

Cover image for JavaScript Promises Explained for Beginners
Akash Kumar
Akash Kumar

Posted on

JavaScript Promises Explained for Beginners

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!");
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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.        │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
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 pending to either fulfilled or rejected, 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
  }
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. You place your order — the Promise is pending
  2. The pizza arrives at your door — the Promise is fulfilled (with the pizza as the value)
  3. 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"
});
Enter fullscreen mode Exit fullscreen mode

.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"
});
Enter fullscreen mode Exit fullscreen mode

.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!");
  });
Enter fullscreen mode Exit fullscreen mode

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) │
                         └──────────┘
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

The key rules of chaining:

  • Whatever you return from 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           
Enter fullscreen mode Exit fullscreen mode

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 — including async/await — will click into place naturally.

Top comments (0)