Architecturally, a Promise is an object that acts as a placeholder for a future value. When we start an asynchronous task like a network request, the engine immediately gives us this object. Initially, it is empty. Later, it will be filled with data or an error.
A Promise can be in exactly one of three states. The state transition is irreversible.
Pending: The initial state. The operation is still in progress.Resolved: The operation completed successfully. The object holds the value.Rejected: The operation failed. The object holds the error.We create a Promise using the new Promise() constructor. This constructor takes a single function argument called the Executor Function.
Crucial Rule: The Executor Function runs Synchronously and Immediately.
When we write new Promise(...), the JavaScript engine executes the code inside that function instantly, on the Call Stack. It does not wait.
console.log("1. Start");
const myPromise = new Promise((resolve, reject) => {
// This Executor runs IMMEDIATELY (Synchronously)
console.log("2. Inside Executor");
// This async part goes to the Web API
setTimeout(() => {
resolve("Data Loaded");
}, 1000);
});
console.log("3. End");Output:
1. Start
2. Inside Executor <-- Proves the executor is synchronous
3. EndNote: Only the
resolve()orreject()calls inside the asynchronous callback happen later. The executor function is executed now.
In previous archive, we saw the danger of passing a callback into a third-party function, giving them control. Promises flip this model.
Instead of passing our logic into analytics.track(), the function returns a Promise object to us.
// Callback Approach (Risky)
analytics.track(data, function () {
// We hope they call this...
});
// Promise Approach (Safe)
const trackingTask = analytics.track(data);
// WE decide when to listen. WE control the next step.
trackingTask.then(() => {
chargeCreditCard();
});We trust the standard Promise API (the browser), not the third-party library, to handle the execution of our code.
Once we have a promise, we attach handlers to react when the state changes. These handlers are placed in the Microtask Queue.
.then(onSuccess, onFailure): Runs if the promise is Fulfilled..catch(onFailure): Runs if the promise is Rejected..finally(onSettled): Runs when the promise is settled (either way).The true power of Promises is Chaining.
Every call to .then() returns a brand new Promise.
.then(), the new promise fulfills with that value.This allows us to flatten the Pyramid of Doom into a vertical, readable chain.
// Flat, readable chain
getUser(userId)
.then((user) => {
// Return a new Promise (async operation)
return getOrders(user.id);
})
.then((orders) => {
// We receive the result of getOrders here
return getPayment(orders[0].id);
})
.then((status) => {
console.log("Payment Status:", status);
})
.catch((err) => {
// Catches errors from ANY step above
console.error("Something went wrong:", err);
});| Feature | Description |
|---|---|
| Executor | (resolve, reject) => ... Runs Synchronously. |
| Immutability | Once settled (Resolved/Rejected), state cannot change. |
| Microtask | .then callbacks go to the high-priority Microtask Queue. |
| Chaining | .then returns a new Promise, allowing linear sequencing. |
.then()?taskA()
.then(() => {
taskB(); // Missing 'return'
})
.then(() => {
console.log("Done");
});The chain is broken. The next .then() will run immediately because the first .then implicitly returns undefined (which is a value) instead of waiting for taskB to finish. taskB becomes a "floating" promise running in the background, detached from the chain.