<28/>
28 Lazy Coder
All

JavaScript Promises Explained: A Complete Beginner’s Guide

Featured Image
The Article
Table of Contents

Have you ever clicked a “Submit” button on a website and seen a little spinning loader before anything happens? During that spin, JavaScript is usually waiting on a Promise.

Promises are one of those topics that trip up a lot of beginners not because the idea is hard, but because most explanations jump straight into confusing words like “asynchronous,” “microtask queue,” and “event loop” before you even know what problem Promises are solving.

So let’s slow down. In this guide, we’ll build up the idea of a Promise from scratch, using an everyday example you already understand. By the end, you’ll know exactly what a Promise is, how to create one, how to use .then(), .catch(), and .finally(), and how to work with multiple Promises at once.

What Is a Promise in JavaScript?

A Promise in JavaScript is an object that represents a task that hasn’t finished yet, but will finish at some point in the future either successfully or with an error.

Here’s a real-life way to think about it. Imagine you order a pizza online. The moment you place the order, you don’t get the pizza instantly. Instead, you get an order confirmation a kind of receipt that says “we’re working on it.” That confirmation can end in one of two ways:

A JavaScript Promise works exactly like that order confirmation. It’s a placeholder for a value you don’t have yet, but you’re guaranteed to get an answer eventually either the task succeeds, or it fails.

Why Do We Need Promises?

Some tasks in JavaScript take time to complete. Common examples include:

These are called asynchronous tasks a fancy word that simply means “doesn’t happen instantly, and the rest of the program keeps running while it waits.”

Before Promises existed, developers handled these delayed tasks using something called callbacks (a function passed into another function, to be run later). If you’re not familiar with functions yet, our guide on JavaScript functions is a good place to start first.

Callbacks worked, but if you needed to run several delayed tasks one after another, your code turned into a deeply nested mess that developers nicknamed “callback hell.” Promises were introduced to clean this up, giving asynchronous code a much more readable, flat structure.

The Three States of a Promise

Every Promise is always in exactly one of these three states:

StateWhat It Means
PendingThe task hasn’t finished yet. Still waiting.
FulfilledThe task finished successfully.
RejectedThe task failed with an error.

Simple analogy: Think of a Promise like a traffic light. It starts on yellow (pending get ready), and then it changes to either green (fulfilled go) or red (rejected stop). Once it changes to green or red, it never goes back to yellow. A Promise can only change state once; after that, it’s considered “settled.”

Creating Your First Promise

You create a Promise using the Promise constructor a built-in JavaScript tool for building new Promise objects. It takes a function as input, and that function gets two special tools to work with: resolve and reject.

javascript

const myFirstPromise = new Promise((resolve, reject) => {
  const taskSucceeded = true;

  if (taskSucceeded) {
    resolve("The task worked!");
  } else {
    reject("The task failed.");
  }
});

The Promise Constructor

Let’s break that code down piece by piece:

resolve() and reject() Explained

Here’s a more realistic example using setTimeout (a built-in function that waits a set number of milliseconds before running code) to simulate a task that takes time, like a network request:

javascript

const orderPizza = new Promise((resolve, reject) => {
  console.log("Order placed. Waiting for the pizza...");

  setTimeout(() => {
    const pizzaIsReady = true;

    if (pizzaIsReady) {
      resolve("Your pizza has arrived!");
    } else {
      reject("Sorry, the pizza shop is closed.");
    }
  }, 2000); // waits 2 seconds
});

At this point, orderPizza exists and is in the “pending” state. Nothing has happened with the result yet we need a way to actually react to it, which is where .then() comes in.

Using a Promise: then, catch, and finally

Once you have a Promise, you attach handlers to it functions that run automatically once the Promise settles (either succeeds or fails).

.then() Handling Success

.then() runs when the Promise is fulfilled. It receives the value passed into resolve().

javascript

orderPizza.then((message) => {
  console.log(message); // "Your pizza has arrived!"
});

.catch() Handling Errors

.catch() runs when the Promise is rejected. It receives the value passed into reject().

javascript

orderPizza
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.log(error); // "Sorry, the pizza shop is closed."
  });

Notice how .then() and .catch() are simply chained onto the Promise, one after another using a dot. This is possible because .then() itself returns a new Promise, letting you keep adding more steps.

.finally() Always Runs

.finally() runs no matter what happened whether the Promise succeeded or failed. It’s perfect for cleanup tasks, like hiding a loading spinner.

javascript

orderPizza
  .then((message) => {
    console.log(message);
  })
  .catch((error) => {
    console.log(error);
  })
  .finally(() => {
    console.log("Order process finished.");
  });

Simple analogy: .then() is like the delivery text you get when your food arrives. .catch() is like the apology text you get if the order gets cancelled. .finally() is like the app closing the order tracker either way, because the order is done win or lose.

Chaining Promises

One of the biggest reasons Promises exist is to let you run several asynchronous steps in order, without nesting them inside each other. This is called chaining.

javascript

function washClothes() {
  return new Promise((resolve) => {
    setTimeout(() => resolve("Clothes washed"), 1000);
  });
}

function dryClothes(message) {
  console.log(message);
  return new Promise((resolve) => {
    setTimeout(() => resolve("Clothes dried"), 1000);
  });
}

function foldClothes(message) {
  console.log(message);
  return new Promise((resolve) => {
    setTimeout(() => resolve("Clothes folded"), 1000);
  });
}

washClothes()
  .then(dryClothes)
  .then(foldClothes)
  .then((message) => console.log(message))
  .catch((error) => console.log("Something went wrong:", error));

Each step waits for the one before it to finish, and each .then() passes its result along to the next step just like a laundry routine where you can’t dry clothes before they’re washed.

Why Chaining Beats Nested Callbacks (Callback Hell)

Before Promises, the same laundry routine using callbacks would look like this functions stacked inside functions inside functions:

javascript

washClothes(function (result1) {
  dryClothes(result1, function (result2) {
    foldClothes(result2, function (result3) {
      console.log(result3);
    });
  });
});

Notice how this drifts further and further to the right with every step. That sideways staircase shape is exactly what developers call “callback hell” hard to read, hard to edit, and easy to break. Promise chaining fixes this by keeping every step at the same indentation level, reading top to bottom like a simple to-do list.

Real-World Example: Fetching Data

The most common place you’ll actually use Promises is when fetching data from a server, using the built-in fetch() function. fetch() sends a request to a URL and returns a Promise that resolves once the response arrives.

javascript

fetch("https://jsonplaceholder.typicode.com/users/1")
  .then((response) => response.json()) // convert the response to usable data
  .then((data) => {
    console.log(data.name); // logs the user's name
  })
  .catch((error) => {
    console.log("Failed to fetch user:", error);
  });

Here, response.json() is itself a Promise too (it takes a moment to read and convert the response), which is why we can chain another .then() right after it. This pattern fetch, convert to usable data, then use it is something you’ll see constantly in real projects.

Promise.all(), Promise.race(), and Friends

Sometimes you don’t want to run tasks one after another you want to run several tasks at the same time and know when they’re all done. JavaScript gives you a few built-in tools for this, all of which take an array of Promises.

Promise.all()

Promise.all() waits for every Promise in the array to succeed, then gives you all the results together as an array. If even one Promise fails, the whole thing fails immediately.

javascript

const promise1 = Promise.resolve("Data A");
const promise2 = Promise.resolve("Data B");
const promise3 = Promise.resolve("Data C");

Promise.all([promise1, promise2, promise3]).then((results) => {
  console.log(results); // ["Data A", "Data B", "Data C"]
});

Use this when: you need all the results before doing anything else like loading a user’s profile, posts, and settings before showing a dashboard.

Promise.race()

Promise.race() doesn’t wait for everyone it settles as soon as the first Promise in the array settles, whether that’s a success or a failure.

javascript

const slowServer = new Promise((resolve) => setTimeout(resolve, 3000, "Slow server"));
const fastServer = new Promise((resolve) => setTimeout(resolve, 1000, "Fast server"));

Promise.race([slowServer, fastServer]).then((winner) => {
  console.log(winner); // "Fast server"
});

Use this when: you only care about whichever result comes back first like racing a request against a timeout.

Promise.allSettled()

Promise.allSettled() is like a more patient version of Promise.all(). It waits for every Promise to finish, regardless of whether each one succeeded or failed, and reports the outcome of each one individually.

javascript

const promiseA = Promise.resolve("Success!");
const promiseB = Promise.reject("Failed!");

Promise.allSettled([promiseA, promiseB]).then((results) => {
  results.forEach((result) => console.log(result.status));
});
// "fulfilled"
// "rejected"

Use this when: you’re running several independent tasks and want to know how each one turned out, even if some fail like uploading five files and reporting which ones succeeded.

Promises vs Async/Await

You’ll often see another way of writing Promise-based code, using the async and await keywords. It’s not a separate feature it’s just a cleaner way to write Promises, so they read like normal step-by-step code instead of chains of .then().

javascript

// Using .then()
function getUser() {
  fetch("https://jsonplaceholder.typicode.com/users/1")
    .then((response) => response.json())
    .then((data) => console.log(data.name))
    .catch((error) => console.log(error));
}

// Using async/await — same result, different style
async function getUser() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
    const data = await response.json();
    console.log(data.name);
  } catch (error) {
    console.log(error);
  }
}

Simple analogy: If .then() chaining is like giving step-by-step instructions over a walkie-talkie, async/await is like writing those same steps down as a simple numbered list. Under the hood, both are still built entirely on Promises async/await is just sugar-coating on top to make the code easier to read.

Common Mistakes Beginners Make with Promises

Quick Comparison Table

ConceptWhat It Does
new Promise()Creates a new Promise with an executor function
resolve(value)Marks the Promise as successful with a result
reject(reason)Marks the Promise as failed with an error
.then()Runs code when the Promise succeeds
.catch()Runs code when the Promise fails
.finally()Runs code no matter what happens
Promise.all()Waits for all Promises to succeed; fails if any one fails
Promise.race()Settles as soon as the first Promise settles
Promise.allSettled()Waits for all Promises, reports every outcome
async/awaitA cleaner syntax for writing Promise-based code

Frequently Asked Questions

What is a Promise in JavaScript, in one sentence?

A Promise is an object that represents a task which hasn’t finished yet, but will eventually either succeed (fulfilled) or fail (rejected).

What’s the difference between a Promise and a callback?

A callback is just a function passed into another function to run later. A Promise is a more structured way to handle that same idea, letting you chain steps with .then() instead of nesting functions inside each other, and giving you a clean, dedicated way to handle errors with .catch().

Do I need to learn Promises before async/await?

Yes, it really helps. async/await is built entirely on top of Promises it’s just a different way of writing the same thing. Understanding how .then(), .catch(), and Promise states work will make async/await click much faster.

What happens if I don’t add a .catch() to a Promise?

If the Promise gets rejected and there’s no .catch() (or a try/catch block, if you’re using async/await) to handle it, you’ll see an “unhandled promise rejection” warning in the console, and that error won’t be dealt with gracefully in your app.

Can a Promise change state more than once?

No. Once a Promise moves from “pending” to either “fulfilled” or “rejected,” that’s final. It can never switch states again this is what makes Promises predictable and safe to work with.

Is Promise.all() the same as running tasks one after another?

No. Promise.all() starts all the given Promises at (roughly) the same time and waits for them all to finish together. Running tasks “one after another” with .then() chaining means each task only starts once the previous one has completed.

Trusted Sources & References

This guide is grounded in official documentation. For deeper reading on any of these topics, these are reliable places to go:

We recommend bookmarking MDN Web Docs it’s the most trusted, community-maintained reference for JavaScript and the web in general.

Continue Learning

Want to build on what you just learned? Check out these related guides on 28LazyCoder:

Explore more tutorials on 28LazyCoder.

AR

Ashutosh Rajbhar

Full-stack developer

3+ years building WordPress, React, and performance-focused web projects.

Related Articles
Previous ← PHP Conditional Statements Explained: A Beginner’s Guide with Examples