Menu

JavaScript Promises: then, catch, and Promise.all Explained

How Promises work in JavaScript — the three states, chaining with then and catch, combining with Promise.all, and writing your own with new Promise.

A Promise Is a Placeholder for a Future Value

When JavaScript needs to do something that takes time — a network request, reading a file, waiting on a timer — it can't hand you the result right away. Instead, it hands you a Promise: an object that represents a value that will exist, eventually.

index.js
Output
Click Run to see the output here.

The first console.log shows a pending Promise. Half a second later, the Promise resolves, and the .then callback runs with the value. The Promise itself is just an object; the magic is that it knows how to notify anyone who's listening when its value arrives.

The Three States

A Promise is always in one of three states:

  • pending — the work is in progress. No value yet.
  • fulfilled — the work succeeded. A value is available.
  • rejected — the work failed. An error is available.

A Promise transitions from pending to either fulfilled or rejected exactly once, and then stays there forever. You can't un-resolve a Promise or resolve it twice.

index.js
Output
Click Run to see the output here.

Promise.resolve(value) builds an already-fulfilled Promise; Promise.reject(error) builds an already-rejected one. Handy for tests and for returning a Promise from a function that sometimes has the answer immediately.

Reading the Value: .then and .catch

You don't unwrap a Promise directly — you pass a callback to .then, and the Promise calls it when the value is ready:

index.js
Output
Click Run to see the output here.

.catch(fn) runs if the Promise rejects. Under the hood, it's shorthand for .then(undefined, fn). A .catch() at the end of a chain handles rejections from any step above it — you don't need one after every .then.

Chaining: Each .then Returns a New Promise

This is the part that trips people up. .then() doesn't just run a callback — it returns a new Promise that resolves to whatever the callback returned. That lets you chain:

index.js
Output
Click Run to see the output here.

Each step feeds the next. If a .then callback returns a Promise, the chain waits for that Promise before continuing — so async steps compose cleanly:

index.js
Output
Click Run to see the output here.

Three sequential async steps, no nesting. Compare that to the same logic written with callbacks and you'll see why Promises caught on.

Errors Fall Through the Chain

A rejected Promise skips every .then until it finds a .catch. That's the whole error-handling model:

index.js
Output
Click Run to see the output here.

Throwing inside a .then rejects the Promise that .then returned. The next .then sees the rejection and passes it along, until .catch swallows it. A single .catch at the end of a chain is usually all you need — and a chain with no .catch at all will produce an "unhandled promise rejection" warning, which you want to fix.

Building Your Own with new Promise

Most of the time you'll consume Promises that libraries give you. Occasionally you need to wrap something that doesn't return one — usually an old-style callback API:

index.js
Output
Click Run to see the output here.

The function you pass to new Promise is called the executor. It gets two arguments: resolve (call it with the success value) and reject (call it with an error). Call one of them exactly once. After that, further calls are ignored.

Two habits that save pain:

  • Only reach for new Promise when wrapping something that isn't already Promise-based. If a function already returns a Promise, just return it.
  • Always reject with an Error object, not a string. Stack traces are worth keeping.

Running Things in Parallel: Promise.all

.then chains run sequentially. When you have several independent async tasks and want them to run at the same time, Promise.all is the tool:

index.js
Output
Click Run to see the output here.

All three timers run concurrently. Promise.all resolves with an array of results in the same order as the input — once every Promise has fulfilled. Total time is roughly 400ms, not 900ms.

The catch: Promise.all rejects as soon as any of its Promises reject, and the other results are lost. That's the right behavior when you need all the pieces (e.g., rendering a page that requires three API calls). When you don't, use allSettled.

When Some Failures Are OK: Promise.allSettled

Promise.allSettled waits for every Promise to finish — fulfilled or rejected — and gives you a report:

index.js
Output
Click Run to see the output here.

Each result is an object: { status: "fulfilled", value } or { status: "rejected", reason }. Useful when partial success is acceptable — logging a batch of events, fetching a bunch of thumbnails, running independent health checks.

Two other combinators worth knowing:

  • Promise.race([...]) — settles as soon as the first Promise settles, either way. Handy for timeouts.
  • Promise.any([...]) — fulfills with the first success and ignores rejections. Rejects only if every Promise rejects.

Promises Are Always Async

Even a Promise that's already resolved calls its .then callback asynchronously — never synchronously, never on the same tick:

index.js
Output
Click Run to see the output here.

Output is before, after, immediate. The .then callback waits for the current code to finish, then runs on the microtask queue. This rule — "a Promise callback never runs synchronously" — is why mixing Promises with synchronous code is predictable: synchronous code always finishes first.

Next: async/await

Chaining .then calls works, but once you have more than two or three steps it starts to look like a staircase. async/await is syntax on top of Promises that lets you write the same logic as if it were synchronous — with try/catch for errors and regular variables for intermediate values. That's next.

Frequently Asked Questions

What is a Promise in JavaScript?

A Promise is an object that represents a value that isn't ready yet — usually the future result of an async operation like a network request. It's always in one of three states: pending, fulfilled, or rejected. You read the eventual value by attaching callbacks with .then() and .catch().

What's the difference between then and catch?

.then(onFulfilled) runs when the Promise resolves successfully and receives the resolved value. .catch(onRejected) runs when the Promise (or any Promise earlier in the chain) rejects and receives the error. A single .catch() at the end of a chain handles failures from any step above it.

What does Promise.all do?

Promise.all([p1, p2, p3]) takes an array of Promises and returns a single Promise that fulfills with an array of all the resolved values — but only once every input Promise has fulfilled. If any one of them rejects, the whole thing rejects immediately. Use Promise.allSettled when you want every result regardless of failures.

Should I use Promises or async/await?

They're the same machinery — async/await is syntax on top of Promises. Most new code reads better with async/await, but you still return Promises, still catch with try/catch or .catch(), and still reach for Promise.all to run things in parallel. Knowing how Promises work makes async/await make sense.

Learn to code with Coddy

GET STARTED