Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises !!

Updated
•11 min read•View as Markdown

A Promise is an object that represents the eventual completion — or failure — of an asynchronous operation.

Think of it like a real-life promise: someone tells you they'll get something done. From that moment, there are only three possibilities — they're still working on it, they came through, or they didn't. JavaScript Promises work exactly the same way.

Every Promise exists in one of three states:

  • Pending — the task is still in progress

  • Fulfilled — the task completed successfully

  • Rejected — the task failed

Simple concept, powerful implications. Let's dig into each of these states and see exactly how Promises work under the hood.

Why promise exits ??

Great question — why do we even need Promises when we already had a way to handle async code?

Before Promises, JavaScript developers relied entirely on callbacks to deal with asynchronous operations. A callback is simply a function you pass into another function, telling it "when you're done, call this." It worked — but it didn't scale well.

Here's what that looked like in practice:

Imagine you're building a movie ticket booking flow. You need to:

With callbacks, this is what that looks like:

getUser(userId, function(err, user) {
  if (err) return handleError(err);

  getCity(user.cityId, function(err, city) {
    if (err) return handleError(err);

    getMovies(city.id, function(err, movies) {
      if (err) return handleError(err);

      getSeats(movies[0].id, function(err, seats) {
        if (err) return handleError(err);

        bookTicket(user.id, seats[0].id, function(err, booking) {
          if (err) return handleError(err);

          sendConfirmation(user.email, booking.id, function(err, receipt) {
            if (err) return handleError(err);

            console.log("Ticket booked!", receipt);
            // we are 6 levels deep
          });
        });
      });
    });
  });
});

here is many problems with this code

Every single level needs its own error check

The code keeps drifting to the right — the "Pyramid of Doom"

If you need to change the flow, you have to untangle the entire nest

Reusing any step elsewhere is nearly impossible

Reading it top to bottom tells you almost nothing about what it does at a glance

This is callback hell. And this is exactly the problem Promises were designed to solve.

Create and consume Promise

Working with Promises has two distinct steps: creating one and consuming one.

Creating a Promise

Promise is a built-in JavaScript class. To use it, you create an instance of it — passing in a function called the executor, which receives two arguments: resolve and reject.

const myPromise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("It worked!");
  } else {
    reject("Something went wrong.");
  }
});

Call resolve(value) when the task completes successfully

Call reject(value) when something goes wrong or the task fails

Whatever you pass into resolve() or reject() is what gets handed off to the consuming side.

this is how we create a promise now we see how to consuming a promise,

Consuming a Promise

Once a promise is created, you listen to its outcome using three methods — .then(), .catch(), and .finally():

myPromise
  .then(result => console.log(result))   // "It worked!"
  .catch(error => console.error(error))  // if rejected
  .finally(() => console.log("Done"));   // always runs

Here's what each one does:

.then() — runs if the promise is resolved. The callback receives whatever value was passed to resolve(). This is where you handle your success case.

.catch() — runs if the promise is rejected, or if any error is thrown along the way. The callback receives whatever was passed to reject() or the error that was thrown.

.finally() — runs no matter what. Whether the promise resolved or rejected, this callback always executes. Great for cleanup tasks like hiding a loading spinner.

Promise Chaining

This is where Promises truly shine — and where they directly solve the callback hell problem.

Promise Chaining lets you handle multiple async operations in a clean, readable, top-to-bottom flow. Instead of nesting callbacks inside callbacks, you simply chain .then() calls one after another. Each .then() waits for the previous step to finish before moving on — making the code read almost like a list of instructions.

The magic is in what you return from a .then() callback. If you return another Promise from inside a .then(), the next .then() in the chain automatically waits for that new Promise to resolve before it runs. This is how you sequence multiple async operations without ever nesting them.

But the best part? Error handling. With callbacks, you had to manually check for errors at every single step — every level of the pyramid had its own error handler. With Promise chaining, you don't need that. A single .catch() at the end of the entire chain catches any error that occurs at any step above it. If something goes wrong in step one, step two, or step five — it skips everything and falls straight down to that one .catch(). Clean, centralized, and impossible to accidentally miss.

This combination — chained .then() calls flowing top to bottom, with a single .catch() guarding the whole thing — is what makes Promise chaining such a massive improvement over the callback approach.

getUser(userId)
  .then(user => getCity(user.cityId))
  .then(city => getMovies(city.id))
  .then(movies => getSeats(movies[0].id))
  .then(seats => bookTicket(userId, seats[0].id))
  .then(booking => sendConfirmation(booking.id))
  .then(receipt => console.log("Ticket booked!", receipt))
  .catch(err => handleError(err)); // handles ALL errors in one place

the same flow with promise chaining is too clean, easy and readable.

Promise Combinators

So far we've been dealing with one Promise at a time. But real applications rarely work that way. You often need to fire off multiple async operations together and handle their results collectively. That's exactly what Promise Combinators are for.

JavaScript gives you four of them — Promise.all(), Promise.allSettled(), Promise.race(), and Promise.any(). Each one behaves differently, and knowing when to use which one is what separates good async code from great async code.

Promise.all() — Everything or Nothing

const [user, movies, cities] = await Promise.all([
  getUser(userId),
  getMovies(),
  getCities(),
]);

Promise.all() takes an array of Promises and runs them all in parallel. It waits for every single one to resolve, then gives you all the results together in an array — in the same order you passed them in, regardless of which one finished first.

But there's a catch. If even one Promise rejects, the entire thing rejects immediately. It doesn't wait for the others. It doesn't give you the ones that succeeded. It just fails. This is called fail-fast behavior.

So use Promise.all() when your tasks are independent of each other but you need all of them to succeed to move forward.

Promise.allSettled() — I Want Every Result, No Matter What

const results = await Promise.allSettled([
  getUser(1),
  getUser(99999),  // this one might fail
  getUser(3),
]);

results.forEach(result => {
  if (result.status === "fulfilled") {
    console.log("Got user:", result.value);
  } else {
    console.error("Failed:", result.reason);
  }
});

Promise.allSettled() also runs everything in parallel — but unlike Promise.all(), it never rejects. It waits for every single Promise to settle, whether that means fulfilled or rejected, and then gives you an array of outcome objects. Each object tells you the status — either "fulfilled" or "rejected" — and the corresponding value or reason.

Use this when you want the results of everything you can get, and a few failures shouldn't ruin the whole operation. Think sending notifications to a list of users — if a few fail, you still want to know which ones succeeded.

Promise.race() — First One Wins

const result = await Promise.race([
  fetchFromServer(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timed out")), 5000)
  ),
]);

Promise.race() runs all Promises in parallel and resolves or rejects with whichever one settles first — fulfilled or rejected. The rest are simply ignored.

Promise.any() — First Success Wins

const asset = await Promise.any([
  fetchFromCDN1(),
  fetchFromCDN2(),
  fetchFromCDN3(),
]);

Promise.any() runs everything in parallel and resolves with the first one that fulfills — completely ignoring any rejections along the way. It only rejects if every single Promise rejects, in which case it throws an AggregateError containing all the rejection reasons.

This is subtly different from Promise.race(). Race cares about the first to settle — fulfilled or rejected. Any cares only about the first to succeed. So if the first two CDNs fail but the third one works, Promise.any() gives you the third result. Promise.race() would have already rejected with the first failure.

async/await

If writing out new Promise(), .then(), and .catch() every time feels verbose and messy, JavaScript has a cleaner way — async/await.

But here's the most important thing to understand before diving in: async/await is not a replacement for Promises. It is Promises. Every async function, every await expression — it's all just Promises running underneath. The only difference is the syntax sitting on top.

This is what developers mean when they say async/await is syntactic sugar — it's a sweeter, more readable way to write the exact same thing. Your code looks like normal, step-by-step synchronous code that any beginner could follow, but under the hood JavaScript is still creating and resolving Promises exactly like before.

Nothing changed. It just got a lot easier to read.

async keyword

if you made a function with before async keyword with it, it automatically returns a promise of that function no matter what you return js automatically warps in promise.resolve() behind the scenes.

and it alos unlock await keyword inside that function without async function await keyword dont allowed.

await keyword

await can only be used inside an async function. When JavaScript hits an await, it pauses that function and waits for the Promise to settle before moving to the next line. While it's waiting, the rest of your program continues running normally — it only pauses inside that one function, not the entire program.

This is the key insight. You get code that looks like it's pausing and waiting, but it's actually non-blocking under the hood. You get the best of both worlds — readable synchronous-style code with the performance of async.

compare to promises

The Promise chain from before:

getUser(userId)
  .then(user => getCity(user.cityId))
  .then(city => getMovies(city.id))
  .then(movies => getSeats(movies[0].id))
  .then(seats => bookTicket(userId, seats[0].id))
  .then(booking => sendConfirmation(booking.id))
  .then(receipt => console.log("Ticket booked!", receipt))
  .catch(err => handleError(err));

The exact same logic with async/await:

async function bookMovie(userId) {
  try {
    const user    = await getUser(userId);
    const city    = await getCity(user.cityId);
    const movies  = await getMovies(city.id);
    const seats   = await getSeats(movies[0].id);
    const booking = await bookTicket(user.id, seats[0].id);
    const receipt = await sendConfirmation(booking.id);

    console.log("Ticket booked!", receipt);
  } catch (err) {
    handleError(err);
  }
}

Same logic. Same Promises. Just reads like a straight list of steps.

common mistake

The most common mistake with async/await is accidentally running things sequentially when they could run in parallel:

// SLOW — each waits for the previous one (unnecessary)
const movies  = await getMovies();   // 1 second
const concerts = await getConcerts(); // 1 second
const sports  = await getSports();   // 1 second
// total: 3 seconds

These three have nothing to do with each other — there's no reason to wait for movies before fetching concerts. The fix is Promise.all():

// FAST — all three run at the same time
const [movies, concerts, sports] = await Promise.all([
  getMovies(),
  getConcerts(),
  getSports(),
]);
// total: 1 second

The rule is simple — if the next await doesn't depend on the result of the previous one, run them in parallel with Promise.all().

Error handling

In async code, things go wrong — networks fail, databases time out, APIs misbehave. The question isn't if an error will occur, it's whether your app handles it like a pro or crashes in silence.

Why async errors are tricky

In synchronous code, an unhandled error throws and you see it immediately. In asynchronous code, the error often happens later — after the call stack has moved on. Without the right patterns, these errors vanish silently, leaving your app in an unknown state.

Approach 1 — Promises with .then() and .catch()

When you're working with Promises, chain a .catch() at the end of your chain. It acts as a safety net — any rejection anywhere in the chain falls through to it.

  promise-style.js

fetchUserData(userId)
  .then((user) => {
    return fetchOrders(user.id);
  })
  .then((orders) => {
    console.log('Orders loaded:', orders);
  })
  .catch((err) => {
    // any error in the chain lands here
    console.error('Something went wrong:', err.message);
  });

Without .catch(), a rejected promise becomes an unhandled promise rejection —in newer Node.js versions this can crash your process entirely.

Approach 2 — async/await with try/catch

async/await makes async code read like synchronous code — and try/catch brings the same familiar error handling from synchronous JavaScript back to the async world.

  async-await-style.js

async function loadUserOrders(userId) {
  try {
    const user = await fetchUserData(userId);
    const orders = await fetchOrders(user.id);
    console.log('Orders loaded:', orders);
  } catch (err) {
    // awaited errors land here — clean and readable
    console.error('Something went wrong:', err.message);
  }
}

The catch block here works exactly like synchronous error handling — any awaited call that rejects will throw into it. This is the preferred pattern in most modern Node.js codebases, including Express middleware.

JavaScript

Part 2 of 14

This JavaScript fundamentals series is designed for beginners who want to understand JavaScript in a simple and practical way. Instead of jumping directly into complex concepts, the series starts from the core building blocks of JavaScript — variables, data types, operators, conditions, loops, functions, arrays, objects, and asynchronous thinking. Each article explains one topic step by step using simple language, relatable examples, and real coding logic, so readers can build confidence gradually and understand how JavaScript actually works in real projects. Whether someone is starting coding for the first time or revising fundamentals, this series creates a strong base for modern web development

Up next

What Control Flow Means in Programming

Imagine you wake up in the morning. You look outside — if it's raining, you grab an umbrella; otherwise, you head out without one. That simple decision you just made? That's control flow. In programmi