Skip to main content
Madhukar
All Articles

JavaScript Promises Explained for Beginners

August 30, 20265 min read
async-programmingprogrammingweb-developmentjavascriptbeginner
JavaScript Promises Explained for Beginners

A promise is a value that isn’t ready yet

You order food delivery. The app doesn’t hand you the food instantly — it hands you a tracking status: your order is being prepared, it’s on the way, and eventually, it either arrives, or something goes wrong and it’s cancelled. A Promise in JavaScript works the exact same way — it’s a placeholder for a value that doesn’t exist yet, but will, eventually, either successfully or not.

What Problem Promises Solve

Before promises, asynchronous code relied entirely on callbacks — functions passed in, to be run once an operation finished. This worked, but multiple dependent asynchronous steps meant nesting callbacks inside callbacks, producing deeply indented, hard-to-follow code (covered in detail in this series’s dedicated callbacks article).

// The callback problem: each step nested inside the last
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getReceipts(orders, (receipts) => {
console.log(receipts);
});
});
});

Promises were introduced specifically to give asynchronous code a cleaner, more predictable shape — representing “a value that will eventually exist” as an actual object you can work with directly, rather than only being reachable through a nested callback.

Promise States (Pending, Fulfilled, Rejected)

Every Promise exists in exactly one of three states at any given moment:

  • Pending — the operation hasn’t finished yet; no result exists yet
  • Fulfilled — the operation completed successfully, and a result is now available
  • Rejected — the operation failed, and an error is now available instead
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data loaded!"); // moves the promise to "fulfilled"
}, 2000);
});

console.log(promise); // Promise { <pending> } — immediately after creation

A Promise starts pending, and moves to either fulfilled or rejected exactly once — and once it does, it stays in that final state permanently. This one-way transition is called being settled.

Basic Promise Lifecycle

Here’s the full journey, from creation to a final result:

const orderFood = new Promise((resolve, reject) => {
const success = true; // imagine this depends on something real

setTimeout(() => {
if (success) {
resolve("Your food has arrived!");
} else {
reject("Delivery failed.");
}
}, 2000);
});
  1. Creation — new Promise((resolve, reject) => {...}) — the executor function runs immediately
  2. Pending — while the operation (here, the setTimeout) is still in progress
  3. Settlement — calling resolve(...) moves it to fulfilled; calling reject(...) moves it to rejected
  4. Handling — code elsewhere reacts to whichever outcome actually happened

Handling Success and Failure

Once you have a Promise, .then() and .catch() let you react to whichever outcome actually happens.

orderFood
.then((result) => {
console.log(result); // "Your food has arrived!"
})
.catch((error) => {
console.log(error); // runs instead, if it was rejected
});
  • .then() runs its function only if the promise is fulfilled, receiving the resolved value
  • .catch() runs its function only if the promise is rejected, receiving the error

A finally block, for either outcome

orderFood
.then((result) => console.log(result))
.catch((error) => console.log(error))
.finally(() => console.log("Order process complete"));

.finally() runs regardless of whether the promise was fulfilled or rejected — useful for cleanup logic (like hiding a loading spinner) that should happen either way.

Promise Chaining Concept

The real payoff of promises appears once you need multiple dependent asynchronous steps — each one waiting on the result of the last. Instead of nesting, .then() calls can be chained, one after another:

getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => getReceipts(orders))
.then((receipts) => {
console.log(receipts);
})
.catch((error) => {
console.log("Something failed:", error);
});

Each .then() receives the previous step's resolved value, and can return a new promise (or a plain value) to continue the chain. The result: a flat, linear sequence of steps, instead of ever-deepening nested callbacks.

Callback vs Promise, side by side

// Callbacks
getUser(id, (user) => {
getOrders(user.id, (orders) => {
console.log(orders);
});
});

// Promises
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => console.log(orders));

Both accomplish the same sequence — but the promise version stays flat, with a single .catch() able to handle an error from any step in the chain, rather than needing a separate check after every nested callback.

Final Takeaway

A Promise is simply a stand-in for a value that doesn’t exist yet — pending while it’s being worked out, then settling permanently into either fulfilled (with a real result) or rejected (with an error), never both, never neither. .then() and .catch() let you react cleanly to whichever outcome actually happens, and chaining .then() calls turns what used to require nested callbacks into a flat, readable sequence of steps. This is exactly the foundation async/await (covered in this series's dedicated article) builds on top of — understanding promises first is what makes that syntax genuinely make sense, rather than feeling like unexplained shorthand.

Frequently Asked Questions

Can a Promise change from fulfilled back to pending, or from rejected to fulfilled?

> No — once a Promise settles (fulfilled or rejected), that outcome is permanent. It cannot change state again, which is exactly what makes Promises predictable and safe to reason about.

What happens if I don’t add a .catch() to a promise chain?

> If the promise ends up rejected and there’s no .catch() anywhere in the chain to handle it, you'll typically see an "unhandled promise rejection" warning or error — it's considered good practice to always include error handling somewhere in a promise chain.

Is a Promise the same thing as async/await?

> No — async/await is built directly on top of Promises, offering a different, more linear-looking syntax for working with the exact same underlying mechanism. Understanding Promises first makes async/await far easier to genuinely understand, rather than just memorize.

Why is chaining better than nesting callbacks for multiple async steps?

> Because each .then() sits at the same indentation level, mirroring the actual order of steps, and a single .catch() can handle failures from any point in the chain — avoiding the deepening indentation and repeated error checks that nested callbacks require.

Originally published by Mr Madhukar

Read the complete article on Medium with full formatting & reader responses.