# JavaScript Promises Explained for Beginners

# Introduction

When JavaScript performs an asynchronous operation, we often don't get the result immediately.

A file may still be loading.

An API request may still be running.

A database query may still be processing.

So how do we represent a result that **doesn't exist yet, but will exist sometime in the future**?

That's where **Promises** come in.

A Promise gives us a way to represent the eventual result of an asynchronous operation.

* * *

# What Problem Do Promises Solve?

Before Promises, callbacks were commonly used to handle asynchronous operations.

A callback basically says:

> "When your work is finished, call this function."

For example:

```js
function readFile(callback) {
    // asynchronous work...

    callback("File data");
}
```

This works well for a simple operation.

But imagine several operations that depend on each other:

```text
Read File
   ↓
Process File
   ↓
Upload File
   ↓
Save Result
```

With callbacks, we may end up nesting one callback inside another:

```js
readFile((data) => {
    processFile(data, (processedData) => {
        uploadFile(processedData, (result) => {
            saveResult(result, () => {
                console.log("Done");
            });
        });
    });
});
```

As the number of operations increases, the code becomes harder to read and maintain.

This is commonly known as **Callback Hell**.

```text
readFile()
   │
   └── processFile()
          │
          └── uploadFile()
                 │
                 └── saveResult()
                        │
                        └── ...
```

Promises provide a cleaner way to represent and connect these asynchronous operations.

* * *

# A Promise as a Future Value

A simple way to understand a Promise is:

> **A Promise represents a value that we don't have yet, but expect to receive in the future.**

Think about ordering food at a restaurant.

You place the order and receive a receipt.

The receipt is not the food.

But it represents an order that will eventually produce a result:

```text
Order Placed
     │
     ▼
   Receipt
     │
     │ waiting...
     ▼
     Food
```

A Promise works similarly:

```text
Async Operation
      │
      ▼
    Promise
      │
      │ waiting...
      ▼
   Result
```

The Promise gives us something to work with **before the final result is available**.

* * *

# Creating a Promise

We can create a Promise using:

```js
new Promise()
```

For example:

```js
function prepareOrder(dish) {
    return new Promise((resolve, reject) => {

        setTimeout(() => {
            resolve({
                dish,
                status: "prepared"
            });
        }, 1000);

    });
}
```

The function passed to `new Promise()` is called the **executor function**.

```js
(resolve, reject) => {
    // executor
}
```

The Promise constructor automatically provides the two functions:

```text
resolve
reject
```

We use them to tell the Promise what happened.

```text
Success
   │
   ▼
resolve(value)

Failure
   │
   ▼
reject(error)
```

* * *

# Promise States

A Promise has three possible states:

```text
                Pending
               /       \
              /         \
             ▼           ▼
        Fulfilled      Rejected
         Success        Failure
```

Let's understand each one.

* * *

## 1\. Pending

When we create a Promise, it starts in the **pending** state.

The asynchronous operation is still running.

```text
Promise
   │
   ▼
Pending
   │
   │ "Still working..."
```

For example:

```js
const orderPromise = prepareOrder("Biryani");
```

At this point, the order may still be getting prepared.

* * *

## 2\. Fulfilled

If the operation succeeds, the Promise becomes **fulfilled**.

```js
resolve(order);
```

The Promise now has a successful result.

```text
Pending
   │
   ▼
Fulfilled
   │
   ▼
Result available
```

* * *

## 3\. Rejected

If something goes wrong, the Promise becomes **rejected**.

```js
reject(error);
```

For example:

```text
Pending
   │
   ▼
Rejected
   │
   ▼
Error available
```

Once a Promise becomes fulfilled or rejected, it is **settled**.

It cannot go back to pending or change from fulfilled to rejected.

* * *

# The Promise Lifecycle

The complete lifecycle looks like this:

```text
                     Promise Created
                           │
                           ▼
                       PENDING
                       /     \
                      /       \
             success /         \ failure
                    ▼           ▼
               FULFILLED     REJECTED
                    │           │
                    ▼           ▼
                  value        error
```

A Promise therefore represents the journey from:

```text
"Not finished yet"
        ↓
"Finished successfully"
```

or:

```text
"Not finished yet"
        ↓
"Something went wrong"
```

* * *

# The Executor Runs Immediately

There is an important detail about creating a Promise.

Consider:

```js
console.log("start");

const promise = new Promise((resolve) => {
    console.log("executor running");

    resolve("done");
});

console.log("end");
```

The output is:

```text
start
executor running
end
```

The executor runs **immediately** when the Promise is created.

So why do we call Promises asynchronous?

Because the asynchronous operation usually happens **inside** the executor.

For example:

```js
const promise = new Promise((resolve) => {

    setTimeout(() => {
        resolve("done");
    }, 1000);

});
```

The executor runs immediately, but the timer completes later.

```text
Promise Created
      │
      ▼
Executor Runs
      │
      ▼
Start Async Operation
      │
      ├──────────► Timer / Network / I/O
      │
      ▼
JavaScript continues
      │
      │
      ▼
Operation completes
      │
      ▼
resolve()
      │
      ▼
Promise fulfilled
```

This distinction is important:

> **The Promise executor runs synchronously. The asynchronous operation inside it may complete later.**

* * *

# Handling a Successful Promise

Once we have a Promise, we need a way to use its result.

That's where `.then()` comes in.

```js
prepareOrder("Biryani")
    .then(order => {
        console.log(order);
    });
```

`.then()` lets us register a function that should run when the Promise is fulfilled.

If we have:

```js
resolve({
    dish: "Biryani",
    status: "prepared"
});
```

then the value received by `.then()` is that object.

```text
Promise
   │
   ▼
Fulfilled
   │
   ▼
.then()
   │
   ▼
Receive resolved value
```

For example:

```js
prepareOrder("Biryani")
    .then(order => {
        console.log(order.dish);
        console.log(order.status);
    });
```

* * *

# Handling Promise Failure

What if the operation fails?

That's where `.catch()` comes in.

```js
prepareOrder("Biryani")
    .then(order => {
        console.log(order);
    })
    .catch(error => {
        console.error(error);
    });
```

If the Promise is rejected:

```js
reject(new Error("Unable to prepare order"));
```

the `.catch()` handler can handle that error.

The flow becomes:

```text
                    Promise
                       │
                 ┌─────┴─────┐
                 ▼           ▼
            Fulfilled     Rejected
                 │           │
                 ▼           ▼
              .then()     .catch()
                 │           │
                 ▼           ▼
              Success      Error
```

This gives us a clean separation between success and failure.

* * *

# Callback vs Promise

Let's compare how the same asynchronous operation can be represented.

### Callback

```js
readFile("data.txt", (err, data) => {
    if (err) {
        console.error(err);
        return;
    }

    console.log(data);
});
```

The function receives a callback and calls it when the operation finishes.

### Promise

```js
readFile("data.txt")
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.error(err);
    });
```

The operation returns a Promise.

We then decide what should happen when that Promise succeeds or fails.

The difference becomes even more visible when multiple operations are involved.

* * *

# Promise Chaining

One of the biggest benefits of Promises is **chaining**.

Suppose we have:

```text
Prepare Order
      ↓
Pickup Order
      ↓
Deliver Order
```

With callbacks, this can become deeply nested.

With Promises:

```js
prepareOrder("Biryani")
    .then(order => pickupOrder(order))
    .then(order => deliverOrder(order))
    .then(order => {
        console.log(order);
    })
    .catch(error => {
        console.error(error);
    });
```

Now the flow is much easier to read.

```text
prepareOrder()
      │
      ▼
   .then()
      │
      ▼
pickupOrder()
      │
      ▼
   .then()
      │
      ▼
deliverOrder()
      │
      ▼
   .then()
      │
      ▼
   Result
```

* * *

# How Does Promise Chaining Work?

The important part is that a `.then()` handler can **return another Promise**.

For example:

```js
prepareOrder("Biryani")
    .then(order => {
        return pickupOrder(order);
    })
    .then(order => {
        return deliverOrder(order);
    });
```

If `pickupOrder()` returns a Promise, the next `.then()` waits for that Promise to settle.

Conceptually:

```text
Promise A
   │
   ▼
.then()
   │
   │ returns Promise B
   ▼
Promise B
   │
   ▼
.then()
   │
   │ returns Promise C
   ▼
Promise C
```

This is what allows us to connect asynchronous operations together without deeply nesting callbacks.

* * *

# Error Propagation in a Chain

One useful property of Promise chains is that a rejection can move through the chain until it is handled.

For example:

```js
prepareOrder("Biryani")
    .then(order => pickupOrder(order))
    .then(order => deliverOrder(order))
    .catch(error => {
        console.error("Something went wrong:", error);
    });
```

If `prepareOrder()`, `pickupOrder()`, or `deliverOrder()` rejects, the chain can reach the `.catch()` handler.

```text
prepareOrder()
      │
      ▼
pickupOrder()
      │
      ▼
deliverOrder()
      │
      ▼
   Success

Any rejection
      │
      ▼
   .catch()
```

This is one of the reasons Promises improve error handling compared with deeply nested callbacks.

* * *

# Promises and the Event Loop

There is one more important thing to understand.

When a Promise settles, its `.then()` or `.catch()` handler does **not** execute immediately in the middle of the current synchronous code.

Promise reactions are handled through the **microtask queue**.

For example:

```js
console.log("start");

Promise.resolve("done")
    .then(value => {
        console.log(value);
    });

console.log("end");
```

Output:

```text
start
end
done
```

Why?

The synchronous code runs first:

```text
start
end
```

Then the Promise handler is processed as a microtask:

```text
done
```

Conceptually:

```text
Synchronous Code
       │
       ▼
Microtask Queue
(Promise handlers)
       │
       ▼
Execute .then()
```

This is why even an already fulfilled Promise doesn't execute its `.then()` handler immediately.

* * *

# A Simple Real-World Analogy

Imagine ordering something online.

When you place an order:

```text
Place Order
     │
     ▼
Order Confirmation
```

The confirmation is not the actual product.

It represents:

> "Your order exists, and the final result will come later."

Then there are possible outcomes:

```text
             Order
               │
               ▼
          Processing
          /        \
         /          \
        ▼            ▼
   Delivered      Failed
```

That's similar to a Promise:

```text
            Promise
               │
               ▼
            Pending
           /       \
          /         \
         ▼           ▼
    Fulfilled     Rejected
```

The Promise is the representation of the future result.

* * *

# Callback vs Promise: The Core Difference

The difference can be summarized like this:

```text
CALLBACK

Start Operation
      │
      ▼
Give function to operation
      │
      ▼
Trust it to call the function
      │
      ▼
Callback receives result
```

While with a Promise:

```text
PROMISE

Start Operation
      │
      ▼
Receive Promise
      │
      ▼
Attach .then() / .catch()
      │
      ▼
Promise settles
      │
      ├──► .then()  → success
      │
      └──► .catch() → failure
```

Promises therefore give us a more structured way to represent and handle asynchronous results.

* * *

# Final Takeaway

The main purpose of a Promise is simple:

> **A Promise represents the eventual result of an asynchronous operation.**

It starts as:

```text
Pending
```

and eventually becomes:

```text
Fulfilled
```

or:

```text
Rejected
```

We handle those outcomes using:

```js
.then()
.catch()
```

And when multiple asynchronous operations depend on each other, we can chain them:

```js
prepareOrder()
    .then(pickupOrder)
    .then(deliverOrder)
    .catch(handleError);
```

Compared with deeply nested callbacks, Promise chains give us:

*   Better readability
    
*   Cleaner asynchronous flow
    
*   Easier error propagation
    
*   A standard way to represent future results
    
*   Easier composition of asynchronous operations
    

The key idea I keep in mind is:

```text
A callback says:

"Call me when you're done."


A Promise says:

"Give me something representing the result,
and I'll decide what to do when it arrives."
```

Once this mental model is clear, concepts like `async/await`, Promise combinators, and the **microtask queue** become much easier to understand.
