What you'll learn
- Quick answer
- What is a promise, really?
- The three states of a promise
- Reading results with .then, .catch and .finally
- How promises escape callback hell
- The chaining rule: return values flow forward
- Running promises together with Promise.all
- Error handling gotchas to avoid
- Our recommendation for beginners
- FAQ
Quick Answer
A JavaScript promise is an object that represents a value you do not have yet: the result of an async task that finishes later. It always lives in one of three states: pending, fulfilled, or rejected. You read its result with .then(), handle errors with .catch(), and run cleanup with .finally(). Promises let you chain steps in a flat, readable line instead of nesting callbacks.
What is a promise, really?
If you have ever loaded data from a server, read a file, or waited for a timer in JavaScript, you have written code that does not finish right away. JavaScript promises are the built-in way to handle these "not ready yet" results cleanly.
A promise is simply an object. Think of it like a token at a tea stall: you order chai, get a token, and keep chatting. The token is not the chai itself; it is a promise that chai will arrive, or that something will go wrong (they ran out of milk). The promise object works the same way: it stands in for a value that will be ready in the future.
const chai = new Promise((resolve, reject) => {
const milkAvailable = true;
if (milkAvailable) {
resolve("Here is your chai");
} else {
reject(new Error("Out of milk"));
}
});The function you pass to new Promise is called the executor. It gets two functions: call resolve(value) when the work succeeds, and reject(error) when it fails. You rarely write new Promise by hand in real projects, since most APIs (like fetch) hand you a promise already, but building one shows exactly what is happening inside. Want the full grounding? Our free JavaScript course walks through this step by step.
The three states of a promise
Every promise is always in exactly one of three states:
- Pending — the work is still running. This is the starting state.
- Fulfilled — the work finished successfully and the promise now holds a value (you called
resolve). - Rejected — the work failed and the promise holds a reason, usually an
Error(you calledreject).
Two rules make promises predictable. First, a promise moves from pending to either fulfilled or rejected, never both. Second, once it settles it is locked forever; its value or error cannot change afterwards. "Settled" just means "no longer pending". Because the result is fixed, you can attach handlers even after the promise has already finished and still get the value back.
Reading results with .then, .catch and .finally
You do not read a promise's value directly. Instead you attach handlers that run when it settles:
.then(onFulfilled)runs when the promise is fulfilled, receiving the value..catch(onRejected)runs when the promise is rejected, receiving the error..finally(onDone)runs either way, which is perfect for cleanup like hiding a loading spinner.
chai
.then((message) => console.log(message)) // "Here is your chai"
.catch((error) => console.log(error.message)) // "Out of milk"
.finally(() => console.log("Order complete"));Note that .finally() does not receive the value or the error. Its only job is to run code that must happen no matter what. A common real example: show a spinner, fetch data, then always hide the spinner.
showSpinner();
fetchData()
.then((data) => render(data))
.catch((error) => showError(error))
.finally(() => hideSpinner());
How promises escape callback hell
Before promises, async code used callbacks: functions passed into other functions to run "when done". Nesting a few of these creates the infamous callback hell, also called the pyramid of doom:
getUser(1, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0], (details) => {
console.log(details);
// error handling? it gets messy fast
});
});
});Each step drifts further to the right, and handling errors at every level is painful. Promises flatten this into a straight, top-to-bottom sequence:
getUser(1)
.then((user) => getOrders(user.id))
.then((orders) => getOrderDetails(orders[0]))
.then((details) => console.log(details))
.catch((error) => console.log(error.message));Same logic, far easier to read, and a single .catch() at the end handles a failure from any step above it. That is the core reason promises exist.
The chaining rule: return values flow forward
The magic of chaining comes from one simple rule: whatever you return inside a .then() becomes the input to the next .then(). Return a plain value and it passes straight through:
Promise.resolve(5)
.then((n) => n * 2) // returns 10
.then((n) => n + 1) // receives 10, returns 11
.then((n) => console.log(n)); // logs 11Here Promise.resolve(5) is a shortcut for a promise that is already fulfilled with the value 5. The powerful part: if you return another promise from a .then(), the chain waits for it to settle before moving on. That is exactly why getOrders(user.id) in the earlier example works, because it returns a promise and the next .then() waits for the orders to arrive.
Gotcha: forgetting to return inside a .then() is the most common beginner mistake. If you do not return the inner promise, the next step runs immediately with undefined instead of waiting for the result.
Running promises together with Promise.all
Chaining runs steps one after another. But sometimes tasks do not depend on each other and you want them to run at the same time. That is where the static combinators come in.
Promise.all() takes an array of promises and gives you one promise that fulfills with an array of all their results, once every one has succeeded:
const p1 = Promise.resolve("html");
const p2 = Promise.resolve("css");
const p3 = Promise.resolve("js");
Promise.all([p1, p2, p3])
.then((results) => console.log(results)); // ["html", "css", "js"]The catch: if any promise rejects, Promise.all() rejects immediately with that error and ignores the rest. When you want every result regardless of failures, use Promise.allSettled() instead, which never rejects and reports the status of each. A quick comparison:
| Method | Waits for all? | Fails fast on one rejection? |
Promise.all | Yes | Yes |
Promise.allSettled | Yes | No |
Promise.race | No | First to settle wins |
Promise.race() settles as soon as the first promise settles, which is handy for building timeouts.
Error handling gotchas to avoid
Good error handling is where beginners slip. A few rules keep you safe:
- Always end a chain with
.catch(). A rejected promise with no catch becomes an "unhandled promise rejection" warning, and can crash a Node.js process. - One
.catch()covers the whole chain. An error thrown or rejected in any earlier step jumps straight to the nearest.catch()below it, skipping the.then()s in between. - Throwing inside
.then()rejects the chain. Athrow new Error(...)inside a handler behaves just likereject, so your.catch()will still catch it.
Promise.resolve()
.then(() => {
throw new Error("Boom");
})
.then(() => console.log("skipped"))
.catch((error) => console.log("Caught:", error.message)); // Caught: BoomNotice the middle .then() never runs. The thrown error skips it and lands in .catch().
Our recommendation for beginners
Promises are the foundation every modern JavaScript async tool is built on, including async/await, which is just friendlier syntax over these same promise objects. Learn promises first and await will make instant sense later.
Our recommendation as you start out:
- Use
fetch()and other promise-based APIs rather than old callback style. - Chain with
.then()and always finish with a single.catch(). - Reach for
Promise.all()when independent tasks can run together to save time. - Remember to
returninside every.then()that produces a value for the next step.
Practice by rewriting a small nested-callback snippet as a promise chain. The "aha" moment comes fast. Ready to go deeper with hands-on exercises? Start the free Priodemy JavaScript course.
Frequently Asked Questions
What are the three states of a JavaScript promise?
A promise is always in one of three states: pending (the async work is still running), fulfilled (it succeeded and holds a value), or rejected (it failed and holds an error). It starts pending and settles into fulfilled or rejected exactly once, then stays locked on that result.
What is the difference between .then() and .catch()?
.then() runs when a promise is fulfilled and receives the resolved value, while .catch() runs when a promise is rejected and receives the error. In practice, .catch(fn) is shorthand for .then(undefined, fn). A single .catch() at the end of a chain will handle a rejection from any step above it.
Are promises the same as async/await?
No, but they are closely related. async/await is newer syntax that lets you write promise-based code as if it were step-by-step normal code. Under the hood it still uses the exact same promise objects: an async function always returns a promise, and await simply pauses until a promise settles. Understanding promises first makes async/await easy to learn.
What happens to Promise.all() if one promise fails?
Promise.all() fails fast. As soon as one promise in the array rejects, the whole thing rejects immediately with that error and ignores the other results. If you need every result even when some fail, use Promise.allSettled() instead, which waits for all of them and reports the status of each without ever rejecting.
Do I always need a .catch() on a promise?
Yes, in almost every case. A rejected promise with no error handler produces an "unhandled promise rejection" warning and can crash a Node.js process. Always end your chain with a .catch(), or handle errors with a try/catch block if you are using async/await.
