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:
function readFile(callback) {
// asynchronous work...
callback("File data");
}
This works well for a simple operation.
But imagine several operations that depend on each other:
Read File
↓
Process File
↓
Upload File
↓
Save Result
With callbacks, we may end up nesting one callback inside another:
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.
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:
Order Placed
│
▼
Receipt
│
│ waiting...
▼
Food
A Promise works similarly:
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:
new Promise()
For example:
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.
(resolve, reject) => {
// executor
}
The Promise constructor automatically provides the two functions:
resolve
reject
We use them to tell the Promise what happened.
Success
│
▼
resolve(value)
Failure
│
▼
reject(error)
Promise States
A Promise has three possible states:
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.
Promise
│
▼
Pending
│
│ "Still working..."
For example:
const orderPromise = prepareOrder("Biryani");
At this point, the order may still be getting prepared.
2. Fulfilled
If the operation succeeds, the Promise becomes fulfilled.
resolve(order);
The Promise now has a successful result.
Pending
│
▼
Fulfilled
│
▼
Result available
3. Rejected
If something goes wrong, the Promise becomes rejected.
reject(error);
For example:
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:
Promise Created
│
▼
PENDING
/ \
/ \
success / \ failure
▼ ▼
FULFILLED REJECTED
│ │
▼ ▼
value error
A Promise therefore represents the journey from:
"Not finished yet"
↓
"Finished successfully"
or:
"Not finished yet"
↓
"Something went wrong"
The Executor Runs Immediately
There is an important detail about creating a Promise.
Consider:
console.log("start");
const promise = new Promise((resolve) => {
console.log("executor running");
resolve("done");
});
console.log("end");
The output is:
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:
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve("done");
}, 1000);
});
The executor runs immediately, but the timer completes later.
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.
prepareOrder("Biryani")
.then(order => {
console.log(order);
});
.then() lets us register a function that should run when the Promise is fulfilled.
If we have:
resolve({
dish: "Biryani",
status: "prepared"
});
then the value received by .then() is that object.
Promise
│
▼
Fulfilled
│
▼
.then()
│
▼
Receive resolved value
For example:
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.
prepareOrder("Biryani")
.then(order => {
console.log(order);
})
.catch(error => {
console.error(error);
});
If the Promise is rejected:
reject(new Error("Unable to prepare order"));
the .catch() handler can handle that error.
The flow becomes:
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
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
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:
Prepare Order
↓
Pickup Order
↓
Deliver Order
With callbacks, this can become deeply nested.
With Promises:
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.
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:
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:
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:
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.
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:
console.log("start");
Promise.resolve("done")
.then(value => {
console.log(value);
});
console.log("end");
Output:
start
end
done
Why?
The synchronous code runs first:
start
end
Then the Promise handler is processed as a microtask:
done
Conceptually:
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:
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:
Order
│
▼
Processing
/ \
/ \
▼ ▼
Delivered Failed
That's similar to a Promise:
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:
CALLBACK
Start Operation
│
▼
Give function to operation
│
▼
Trust it to call the function
│
▼
Callback receives result
While with a Promise:
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:
Pending
and eventually becomes:
Fulfilled
or:
Rejected
We handle those outcomes using:
.then()
.catch()
And when multiple asynchronous operations depend on each other, we can chain them:
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:
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.
Comments
Join the discussionNo comments yet. Be the first to comment.