DEV Community

Cover image for The Ultimate Promise Deep Dive — Resolve, Reject, Then, and Async/Await Demystified
Rahul Sharma
Rahul Sharma

Posted on

The Ultimate Promise Deep Dive — Resolve, Reject, Then, and Async/Await Demystified

Most developers don’t write raw Promises every day — we use tools like useQuery, axios, or helper libraries that wrap everything for us. But when it comes to debugging, reading polyfills, or understanding what those tools are actually doing, everything goes back to Promises.

And especially today, when many beginners rely on AI to generate code, it's super important to truly understand what Promises are doing under the hood.

So let’s start slow, simple, and clear. Step-by-step.— a zero-to-hero journey into JavaScript Promises. चलो शुरू करते हैं.

What is a Promise? (I like this basic and deep definition of a Promise)

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Introduction

Okay, so you’re here — which means either an interview is coming up soon, or you’re just genuinely curious about how Promises actually work. Both reasons are perfect.

Before Promises, JavaScript handled async work using callbacks (and yes, callbacks are still important in many real-world cases — we’ll cover that in a separate blog). But back then, callbacks often led to messy, nested “callback hell.”

Promises solved that problem, and later async/await made async code feel even smoother.

We’ll go step by step — from basics to advance.

What Exactly Is a Promise?

A Promise is simply a placeholder for a value that you will receive in the future — not right now.
The easiest way to imagine it is like ordering food at a restaurant:
you get a token first, and the actual food (the value) comes later.

A Promise can be in three states:

  • pending → the async work is still happening
  • fulfilled → the async work finished successfully
  • rejected → the async work failed

And once a Promise becomes fulfilled or rejected, its state is locked forever — it cannot change again.

Creating a Promise (executor, resolve, reject)

To create a Promise, you use the new Promise() constructor.
It takes a function (called the executor) with two arguments:

resolve → call this when the async work succeeds
reject → call this when something goes wrong

Here’s the example:

const myPromise = new Promise((resolve, reject) => {
  // Simulating async work (like API call)
  setTimeout(() => {
    const success = true; // change to false to test reject

    if (success) {
      resolve("Task completed!");
    } else {
      reject("Something went wrong!");
    }
  }, 1000);
});

Enter fullscreen mode Exit fullscreen mode

This code creates a Promise that:
waits 1 second
then either resolves with "Task completed!"
or rejects with "Something went wrong!"

But creating a Promise alone does nothing —
you must attach .then() / .catch() to use the result.

Consuming a Promise: then(), catch(), finally()

Once you create a Promise, you need to “consume” it using:

  • .then() → runs when the Promise is fulfilled (success)
  • .catch() → runs when the Promise is rejected (failure)
  • .finally() → always runs, success or failure

Here’s a simple example using the Promise we created earlier:

myPromise
  .then((result) => {
    console.log("Success:", result);
  })
  .catch((error) => {
    console.log("Error:", error);
  })
  .finally(() => {
    console.log("Promise done!");
  });

Enter fullscreen mode Exit fullscreen mode

How this works:

  • If the Promise resolves → the code inside .then() runs
  • If it rejects → the code inside .catch() runs
  • And .finally() runs in both cases (cleanup, loaders, etc.)

Promise Chaining (How values flow)

One of the best features of Promises is chaining — the output of one .then() becomes the input of the next .then().

Think of it like passing a value step-by-step through a pipeline.

Here’s a simple example:

Promise.resolve(1)
  .then((num) => {
    console.log("Step 1:", num); // 1
    return num + 1; // returns 2
  })
  .then((value) => {
    console.log("Step 2:", value); // 2
    return value * 10; // returns 20
  })
  .then((finalValue) => {
    console.log("Final Step:", finalValue); // 20
  });

Enter fullscreen mode Exit fullscreen mode

✔ How it works:

  • The first .then() receives 1
  • It returns 1 + 1 = 2
  • The second .then() receives 2
  • It returns 2 * 10 = 20
  • The last .then() receives 20 ** Every .then() creates a new Promise internally, making chaining possible.**

🔥 Important:

If you don’t return anything inside a .then(), the next .then() receives undefined. (Good for interview question)

example of promise

Error Handling in Promises

Errors in Promises work in a very simple way:

  • If any .then() throws an error → the chain jumps to .catch().
  • After a .catch(), the chain continues normally (unless you re-throw).

Here’s a clean example:

Promise.resolve("Start")
  .then((val) => {
    console.log(val); // Start
    throw new Error("Something broke!");
  })
  .then(() => {
    // This will NOT run
    console.log("This will be skipped");
  })
  .catch((err) => {
    console.log("Caught:", err.message); // Caught: Something broke!
  })
  .then(() => {
    console.log("Chain continues after catch"); 
  });

Enter fullscreen mode Exit fullscreen mode

✔ What’s happening?

  • The first .then() runs → prints "Start"
  • It throws an error
  • The chain skips all other .then() and jumps to .catch()
  • After .catch(), the chain continues with the next .then()

🔥 Bonus Tip

If you re-throw inside .catch(), the error will go to the next .catch().

Here you go — a solid list of Promise-based interview coding questions.
These are the ones asked most commonly in frontend + JS rounds, and perfect for devs who preparing for interviews.

Top comments (0)