Learnitweb

Understanding Promises in JavaScript

In this section, we are going to explore Promises in JavaScript — one of the most important concepts for handling asynchronous operations in a clean and readable way.

Promises were introduced to solve many of the problems caused by callbacks, especially callback hell. Before we see how promises work in practice, we must clearly understand what a promise is, why it exists, and how it behaves internally.

What Is a Promise?

A Promise is a special JavaScript object that represents the eventual result of an asynchronous operation.

In simple words:

A promise is a placeholder for a value that is not available yet, but will be available in the future.

It does not immediately contain the result. Instead, it represents the future completion (or failure) of an asynchronous task.

A Simple Real-World Analogy

Imagine you order food at a restaurant.

  • You place the order.
  • The food is not ready immediately.
  • You receive a token or receipt.
  • That receipt represents a promise that your food will arrive later.

Similarly, in JavaScript:

  • You request an asynchronous operation.
  • You immediately receive a promise object.
  • That promise will eventually resolve with the actual result.

Example Scenario: Reading a File Asynchronously

Let’s imagine a function that reads the contents of a file.

  • Reading a file takes time.
  • It may take around 3 seconds.
  • During those 3 seconds, the result is not available.

So the question is:

How do we work with a value that doesn’t exist yet?

Two Ways to Handle Asynchronous Results

There are two common approaches:

1. Using Callbacks

In this approach:

  • We pass a callback function to the asynchronous function.
  • Once the operation finishes, the callback is executed.
  • The result is passed into the callback.

This approach works, but as we saw earlier, it can easily lead to callback hell, especially when multiple asynchronous steps are chained together.

function readFile(callback) {
  setTimeout(() => {
    const data = "File content loaded";
    callback(data);
  }, 3000);
}

2. Using Promises

Instead of passing a callback, the function returns a Promise immediately.

This promise:

  • Represents the future result.
  • Can be stored in a variable.
  • Can be passed to other functions.
  • Can be used before the actual value exists.

This makes the code more flexible and easier to reason about.

function readFile() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;

      if (success) {
        resolve("File content loaded");
      } else {
        reject("Failed to read file");
      }
    }, 3000);
  });
}

What Does a Promise Contain?

A promise internally has two important properties:

1. State (Status)

The state tells us what stage the promise is currently in.

A promise can be in one of three states:

Pending

  • This is the initial state.
  • The asynchronous operation has started.
  • The result is not yet available.
  • The promise is neither fulfilled nor rejected.

In our file-reading example, the promise remains in the pending state during the 3 seconds while the file is being read.

Fulfilled

  • The operation completed successfully.
  • The promise now contains the final result.
  • The value becomes available for use.

For example, once the file is successfully read, the promise moves into the fulfilled state.

Rejected

  • The operation failed.
  • An error occurred during execution.
  • The promise contains the reason for failure.

For example, if the file does not exist or cannot be read, the promise is rejected.

Why Promises Are Useful

Promises provide several important advantages:

  • They allow asynchronous code to look more structured and predictable.
  • They reduce deeply nested callback structures.
  • They make error handling easier and more consistent.
  • They allow chaining operations in a clean and readable way.
  • They integrate naturally with modern JavaScript features like async and await.

Why Promises Are Better Than Callbacks

Compared to callbacks, promises:

  • Improve readability
  • Avoid deeply nested code
  • Make error handling centralized
  • Make complex async flows easier to manage

This is why modern JavaScript heavily relies on promises under the hood.

Creating a Promise in JavaScript

JavaScript provides a built-in class called Promise.

To create a promise, we use the new Promise() constructor.

Syntax

const myPromise = new Promise((resolve, reject) => {
  // asynchronous logic here
});

Understanding the Promise Constructor

The Promise constructor accepts one required argument — a function.

This function is commonly called the executor function.

Important facts about the executor function:

  • It runs immediately when the promise is created.
  • It receives two arguments:
    • resolve → used when the operation succeeds
    • reject → used when the operation fails

Both resolve and reject are functions.

Example 1: Creating a Basic Promise

const myPromise = new Promise((resolve, reject) => {
  console.log("Promise is created");
});

When this code runs:

  • The promise is created immediately.
  • The executor function runs instantly.
  • The promise is still in the pending state.

Inspecting the Promise State

If we log the promise to the console:

console.log(myPromise);

You will see something like:

Promise { <pending> }

If you expand it in the browser console, you’ll see:

  • PromiseState: “pending”
  • PromiseResult: undefined

This tells us:

  • The promise has not yet been fulfilled or rejected.
  • No value has been assigned yet.

Resolving a Promise

To move a promise from pending → fulfilled, we use the resolve() function.

Example:

const myPromise = new Promise((resolve, reject) => {
  resolve("Promise resolved successfully");
});

Now, if you log it:

console.log(myPromise);

You will see:

  • PromiseState: "fulfilled"
  • PromiseResult: "Promise resolved successfully"

This means the promise completed successfully and now holds a value.

Rejecting a Promise

To indicate failure, we use the reject() function.

Example:

const myPromise = new Promise((resolve, reject) => {
  reject("Something went wrong");
});

Now the promise state becomes:

  • PromiseState: "rejected"
  • PromiseResult: "Something went wrong"

Important Note About Rejected Promises

If a promise is rejected and no error handler exists, JavaScript will show an error in the console.

That’s why rejected promises should always be handled using .catch() or a try/catch block (with async/await).

Visualizing Promise States

Pending
   |
   |---- resolve(value)  → Fulfilled
   |
   |---- reject(reason)  → Rejected

Once a promise changes state, it becomes immutable — it cannot change again.

Example: Simulating an Asynchronous Operation

Let’s simulate a delay using setTimeout.

const delayedPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Operation completed successfully");
  }, 3000);
});

console.log(delayedPromise);

What happens here?

  • Immediately after execution → promise is pending
  • After 3 seconds → promise becomes fulfilled

Using the Promise Result

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

Output after 3 seconds:

Operation completed successfully

Handling Rejection

const errorPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("Operation failed");
  }, 2000);
});

errorPromise
  .then(result => console.log(result))
  .catch(error => console.error(error));

Important Rule About Promises

A promise can change its state only once.

That means:

  • pending → fulfilled is allowed
  • pending → rejected is allowed
  • fulfilled → rejected is not allowed
  • rejected → fulfilled is not allowed

Let’s prove this behavior with code.

Example 1: Resolving a Promise Multiple Times

Let’s create a promise that tries to:

  1. Resolve once
  2. Resolve again
  3. Reject afterward
const myPromise = new Promise((resolve, reject) => {
  resolve("First resolved value");
  resolve("Second resolved value");
  reject("This should not work");
});

console.log(myPromise);

What happens?

When we run this code, the output will show:

Promise { <fulfilled>: "First resolved value" }

Explanation

  • The first call to resolve() changes the promise state from pending → fulfilled.
  • After that, the promise becomes immutable.
  • Any further calls to resolve() or reject() are completely ignored.

Even though we tried:

  • resolving again
  • rejecting afterward

none of those had any effect.

Example 2: Rejecting a Promise First

Now let’s reverse the order.

This time:

  1. We reject the promise first.
  2. Then we try to resolve it.
const myPromise = new Promise((resolve, reject) => {
  reject("Something went wrong");
  resolve("This will never run");
  resolve("Neither will this");
});

console.log(myPromise);

Output:

Promise { <rejected>: "Something went wrong" }

What Happened Here?

  • The promise entered the rejected state immediately.
  • The rejection reason became the final value.
  • Any later calls to resolve() were ignored.

This confirms that rejected is also a final state.