Skip to main content

Command Palette

Search for a command to run...

JavaScript Promises Explained for Beginners

Published
7 min read
JavaScript Promises Explained for Beginners

When beginners first encounter asynchronous JavaScript, it usually feels a little weird.

You call a function, but the result does not come back immediately. Maybe it is waiting for an API response, a file to load, or a timer to finish. And suddenly, the normal top-to-bottom flow of code starts feeling less obvious.

This is the exact problem promises were designed to improve.

Promises gave JavaScript a cleaner way to handle asynchronous work without turning code into a mess of nested callbacks.


What Problem Promises Solve

Before promises became common, asynchronous code often relied heavily on callbacks.

A callback is just a function passed into another function to run later. That works, but once multiple async steps are involved, the code can become hard to read very quickly.

For example, the flow starts looking like this:

getUser(function(user) {
  getPosts(user.id, function(posts) {
    getComments(posts[0].id, function(comments) {
      console.log(comments);
    });
  });
});

This style works, but it becomes deeply nested and harder to follow. That is one reason people talk about callback hell.

Promises solve this by giving asynchronous code a more structured format.

Instead of saying, “run this callback when done,” a promise says:

“I represent a value that will be available in the future.”

That is the core idea.


Think of a Promise as a Future Value

A promise is an object that represents the result of an asynchronous operation that has not finished yet.

You do not have the value right now, but you expect to get it later.

A real-life analogy is ordering food online.

  • You place the order

  • the food is not in your hands yet

  • the order is still being processed

  • later it either arrives successfully or it fails

That is basically how a promise works.

It begins in a waiting state, and later settles into either success or failure.


Promise States

A promise has three main states:

1. Pending

The operation is still in progress. Nothing has completed yet.

2. Fulfilled

The operation completed successfully, and the promise now has a result.

3. Rejected

The operation failed, and the promise now has an error or failure reason.

You can think of the lifecycle like this:

Pending → Fulfilled
Pending → Rejected

A promise starts as pending, and then eventually becomes either fulfilled or rejected.

Once that happens, it is settled. It does not go back.


Basic Promise Lifecycle

Let’s create a simple promise.

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

  if (success) {
    resolve("Data loaded successfully");
  } else {
    reject("Something went wrong");
  }
});

Here is what is happening:

  • new Promise(...) creates a promise

  • resolve(...) marks it as fulfilled

  • reject(...) marks it as rejected

At first, the promise is pending. Then based on the condition, it moves to either fulfilled or rejected.

That is the basic lifecycle.


Handling Success and Failure

Promises are usually handled using .then() and .catch().

Handling success

myPromise.then((result) => {
  console.log(result);
});

If the promise is fulfilled, the value passed to resolve() becomes available inside .then().

Handling failure

myPromise.catch((error) => {
  console.log(error);
});

If the promise is rejected, the value passed to reject() becomes available inside .catch().

Handling both together

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

This makes async code much more readable than manually nesting callbacks for every possible outcome.


A Simple Delayed Example

Promises become easier to understand when you see them with something asynchronous, like a timer.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Timer finished");
  }, 2000);
});

promise.then((message) => {
  console.log(message);
});

What happens here?

  1. The promise is created

  2. It stays pending for 2 seconds

  3. After 2 seconds, resolve() is called

  4. The promise becomes fulfilled

  5. .then() receives the result

So the promise acts like a container for a value that shows up later.


Compare Promises with Callbacks

Here is a very simple callback-based style:

function fetchData(callback) {
  setTimeout(() => {
    callback("Data received");
  }, 1000);
}

fetchData(function(result) {
  console.log(result);
});

This is okay for one step.

Now compare the promise version:

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data received");
    }, 1000);
  });
}

fetchData().then((result) => {
  console.log(result);
});

At first glance, both may look similar. But promises become much more useful when multiple asynchronous steps are involved.

They make the flow easier to structure and easier to chain.

That is one of the biggest readability improvements they bring.


Promise Chaining Concept

One of the most useful features of promises is chaining.

This means you can take the result from one .then() and pass it into the next step.

Example:

function getNumber() {
  return Promise.resolve(5);
}

getNumber()
  .then((num) => {
    return num * 2;
  })
  .then((result) => {
    console.log(result);
  });

Output:

10

What is happening here?

  1. getNumber() returns a promise

  2. the first .then() gets 5

  3. it returns 10

  4. the next .then() receives that 10

This creates a clean flow where each step can build on the previous one.

That is much nicer than deeply nesting one callback inside another.


Chaining Async Steps

Now let’s look at a more realistic sequence.

function stepOne() {
  return Promise.resolve("Step 1 complete");
}

function stepTwo() {
  return Promise.resolve("Step 2 complete");
}

stepOne()
  .then((result) => {
    console.log(result);
    return stepTwo();
  })
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log("Error:", error);
  });

This is the promise-based way of saying:

  • do step one

  • when that finishes, do step two

  • if anything fails, handle the error

That flow is much easier to read than nested callbacks.


Why Promises Improve Readability

Promises improve readability in a few important ways.

First, they reduce callback nesting.

Second, they separate success handling and failure handling more clearly.

Third, they let asynchronous code read more like a sequence of actions rather than a pyramid of indentation.

That does not mean promises magically make async code simple, but they definitely make it more manageable.

This is exactly why they became such an important part of modern JavaScript.


A Good Beginner Mental Model

A useful way to think about promises is this:

  • A promise is a placeholder for a future result

  • It starts in a waiting state

  • It eventually succeeds or fails

  • You handle success with .then()

  • You handle failure with .catch()

That mental model is enough to make most beginner examples click.


Final Thoughts

Promises were introduced to make asynchronous JavaScript easier to read and easier to manage.

They solve a real problem: callback-heavy code quickly becomes hard to follow. By representing a future value and giving developers clear ways to handle success, failure, and multi-step flows, promises made async programming much cleaner.

The biggest ideas to remember are:

  • promises represent future values

  • they have three states: pending, fulfilled, and rejected

  • .then() handles success

  • .catch() handles failure

  • chaining helps structure multiple async steps cleanly

Once you understand promises, topics like async/await become much easier too, because async/await is built on top of the same idea.