Promises

An object that represents a value you'll get later, not right now.

What is it?

Some tasks don't finish instantly — fetching data from a server, reading a file, waiting a few seconds. JavaScript can't just pause and wait around, because that would freeze the whole page. Instead, it needs a way to say "start this task, and let me know when it's done."

A Promise is an object that represents a value that isn't ready yet, but will be — either successfully (resolved) or unsuccessfully (rejected). You attach instructions for what to do in each case using .then() and .catch().

Explain like I'm 10

A promise is like a food delivery tracking number. You don't have the food yet, but you have something that represents it — and you can be notified the moment it arrives, or if the order fails.

Examples

Creating and using a promise

function waitOneSecond() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Done waiting!");
    }, 1000);
  });
}

waitOneSecond()
  .then((message) => console.log(message)) // logs "Done waiting!" after 1s
  .catch((error) => console.log("Something went wrong:", error));

A promise that can reject

function fetchUser(id) {
  return new Promise((resolve, reject) => {
    if (id <= 0) {
      reject(new Error("Invalid user id"));
      return;
    }
    resolve({ id, name: "Amara" });
  });
}

fetchUser(-1)
  .then((user) => console.log(user))
  .catch((error) => console.log("Failed:", error.message)); // "Failed: Invalid user id"

reject(...) settles the promise as failed instead of successful, and control jumps straight to .catch(), skipping .then() entirely.

How it works

A promise starts in a "pending" state. When the task finishes, it either calls resolve(value) — moving the promise to "fulfilled" and triggering any .then() callbacks — or calls reject(error), moving it to "rejected" and triggering .catch(). Once settled, a promise's outcome never changes again.

Promise created (pending)
       ↓
Task runs in the background
       ↓
   ┌───────┴───────┐
success           failure
   ↓                 ↓
resolve(value)   reject(error)
   ↓                 ↓
.then() runs     .catch() runs

Why does it exist?

Before promises, handling multiple sequential async tasks (like "fetch a user, then fetch their orders, then fetch order details") led to deeply nested callbacks that were hard to read and error-prone. Promises give asynchronous code a consistent shape and let errors be handled in one place.

When to use it

Reach for a promise anytime you're doing something that finishes later, not immediately — fetching data, reading a file, waiting on a timer — and you want a clean way to say what happens on success versus failure.

When not to use it

You don't need a promise for something that finishes instantly — wrapping simple, immediate work in one just adds overhead. And in most modern code you'll rarely write .then() chains by hand; reach for async/await on top of promises instead, and use raw promises mainly when building the underlying async function itself.

Common mistakes

  • Forgetting to add a .catch(), so errors disappear silently.

  • Nesting .then() calls instead of chaining them, recreating the exact mess promises were meant to fix.

  • Forgetting to return a value inside a .then(), breaking the chain for the next .then().

Practice exercises

  1. Easy:

    Create a promise that resolves with your name after 500ms, and log it with .then().

  2. Medium:

    Create a promise that randomly resolves or rejects, and handle both cases.

  3. Hard:

    Chain three promises together, each depending on the previous result, using .then().

Interview questions

What are the three states a promise can be in, and can it move backward between them?

Pending, fulfilled, and rejected. A promise can only move forward — pending to fulfilled, or pending to rejected — and once it lands on either settled state it stays there forever; nothing can move it back to pending or flip it to the other outcome.

What's the difference between `.then()` and `.catch()`?

.then(onFulfilled, onRejected) can handle both outcomes directly; .catch(onRejected) is really just shorthand for .then(undefined, onRejected), handling only the rejection path. .catch() is preferred for readability because it also catches rejections and thrown errors from earlier .then() callbacks in the chain, not just the original promise.

What does `Promise.all()` do, and how does it behave if one promise rejects?

It takes an iterable of promises and returns a single promise that fulfills with an array of all the results, but only once every one of them fulfills. If any single promise rejects, Promise.all() immediately rejects with that same reason — it doesn't wait for the others to settle, though their work keeps running in the background since promises can't be cancelled.

What does `Promise.allSettled()` do, and how does it differ from `Promise.all()`?

It waits for every promise to settle, whether fulfilled or rejected, and always resolves (never rejects) with an array of result objects like {status: 'fulfilled', value} or {status: 'rejected', reason}. Unlike Promise.all(), one failure doesn't short-circuit the whole thing — you get a full report of every outcome.

What does `Promise.race()` do?

It returns a promise that settles as soon as the first of the input promises settles, adopting that one's outcome — fulfilled or rejected — whichever gets there first wins, and the rest are simply ignored once that happens.

What does `Promise.any()` do, and how is it different from `Promise.race()`?

It resolves as soon as the first promise fulfills, ignoring rejections along the way. Unlike Promise.race(), a single rejection doesn't end things early — Promise.any() only rejects if every promise in the input rejects, and it does so with an AggregateError bundling all the individual reasons.

You need to run four independent API calls and proceed only if all succeed, failing fast if any fails. Which combinator fits, and why not the others?

Promise.all() — it gives you all four results together and rejects immediately on the first failure, matching 'all must succeed.' Promise.allSettled() wouldn't fail fast, since it always waits for every one to finish; Promise.race()/Promise.any() only care about a single winner, not all four.

Once a promise has settled, can calling `resolve()` again change its value?

No. A promise can only settle once — after the first call to resolve or reject inside the executor, every later call to either is silently ignored. This is what makes a promise's outcome immutable once decided.

What is a 'thenable', and why does it matter for promises?

Any object with a .then(onFulfilled, onRejected) method, even if it isn't a real Promise instance. Promise machinery treats thenables specially — Promise.resolve(thenable) and returning a thenable from a .then() callback both 'unwrap' it by waiting for it to settle, rather than treating it as a plain value, which is how other libraries interoperate with native promises.

What happens if you `return` a promise from inside a `.then()` callback?

The outer chain waits for that returned promise to settle before moving on, and its resolved value — not the promise itself — is passed to the next .then(). This automatic flattening is what lets you chain sequential async steps instead of ending up with nested promises.

Predict the output: a chain starts with `Promise.resolve(1)`, then adds 1, then throws inside the next `.then()`, then has a final `.then()` that logs, then a `.catch()` that logs the error message. What actually logs?

Only the .catch() handler's log runs. The thrown error inside the second .then() rejects the chain at that point, which skips every following .then() — they only run on fulfillment — until it reaches the first handler that can deal with a rejection, the .catch().

If you forget to add a `.catch()` to a chain that ends up rejecting, what actually happens?

It becomes an 'unhandled promise rejection.' In a browser this fires an unhandledrejection event and typically logs a warning; in Node.js it logs a warning by default and, depending on version and configuration, can crash the process. It isn't silently swallowed — it's meant to surface as a bug.

Can a `.catch()` handler catch an error thrown by an earlier `.then()` in the same chain?

Yes — a rejection or thrown error at any point in a chain skips forward past every .then() until it finds the next rejection handler, whether that's the onRejected argument to .then() or a .catch(). A single .catch() at the end of a chain can therefore catch failures from any step before it.

What does `.finally()` do, and does its callback receive the resolved value or rejection reason?

It runs after the promise settles regardless of outcome, useful for cleanup like hiding a loading spinner. Its callback receives no arguments at all, and .finally() passes the original outcome through unchanged to the next link in the chain — unless the .finally() callback itself throws or returns a rejected promise, which overrides it.

What's the difference between `Promise.resolve(x)` when `x` is a plain value versus when `x` is already a promise?

If x is a plain value, you get a new promise already fulfilled with it. If x is already a promise or thenable, Promise.resolve(x) doesn't wrap it in another layer — it returns a promise that follows x's own eventual state, so you never end up with a 'promise of a promise.'

Does the executor function passed to `new Promise((resolve, reject) => {...})` run immediately, or only once something calls `.then()`?

Immediately, synchronously, the moment the promise is constructed — promises are eager, not lazy. .then() only controls when you're notified of the result; it doesn't trigger the work to start.

Predict the output: a synchronous log, then constructing a `new Promise` whose executor logs synchronously before resolving, with a `.then()` attached, then a final synchronous log after the constructor call.

The order is: the first log, the executor's own log, the final log, then the .then() callback's log last. The executor body runs synchronously the instant the promise is constructed, but the .then() callback is queued as a microtask and only runs after all the current synchronous code finishes.

If the executor function passed to `new Promise()` throws synchronously, what happens to the promise?

It's automatically caught and treated the same as calling reject(error) — the promise settles as rejected with that thrown error. You don't need a manual try/catch inside the executor purely to convert a synchronous throw into a rejection.

What happens if a `.then()` callback itself throws an error?

The promise returned by that .then() call rejects with the thrown error, which then propagates down the chain the same way an explicit reject() would, until something catches it.

You call `.then()` twice on the same promise from two separate variables, instead of chaining them. Does the second `.then()` see the value transformed by the first?

No — calling .then() twice on the same promise creates two independent branches that both receive the original promise's resolved value, not each other's return values. Only chaining, promise.then(...).then(...), passes a value from one step into the next.

How would you convert an older callback-style function into one that returns a promise?

Wrap the call in new Promise((resolve, reject) => {...}), where the executor calls the original function and its error-first callback drives resolve/reject based on whether an error came back. Node's built-in util.promisify automates exactly this pattern.

How would you add a timeout to a promise-based operation that might hang forever?

Race it against a promise that rejects after a delay, using Promise.race() on an array containing both the real operation and a setTimeout-based rejection. Whichever settles first — the real result or the timeout — determines the outcome, though the loser keeps running in the background since it can't be cancelled.

Since regular JavaScript promises can't be cancelled, how do people typically stop unneeded async work early anyway?

By using a separate cooperative-cancellation signal, most commonly AbortController/AbortSignal — the promise-producing operation (like fetch) checks the signal and rejects or stops on its own when told to abort; the promise object itself never gains a real 'cancel' method.

What does `Promise.all()` resolve with when the input array mixes real promises with plain, non-promise values?

A plain, non-promise value is treated as already-resolved with itself, so it passes straight through in the results array alongside the resolved values of the actual promises, in the same positions they were given.

Why does `Promise.any()` reject with an `AggregateError` instead of a single error?

Because it only fails when every input promise has rejected, and no single reason fully explains the failure — an AggregateError bundles all the individual rejection reasons into one object, via its .errors array, so none of that information is lost.

You have three independent, unrelated async calls that don't depend on each other's results. What's wrong with awaiting them one at a time instead of using `Promise.all()`?

Awaiting them sequentially runs them one after another, so the total time is roughly the sum of all three durations. Starting them together, or wrapping them in Promise.all(), lets them run concurrently, so the total time is closer to the slowest single one, not the sum.

What's the difference in behavior between attaching two separate `.then()` calls to the same promise versus chaining them?

Attaching two separate .then() calls creates two independent listeners that both receive the same original resolved value. Chaining them makes the second one run only after the first finishes, and it receives whatever the first one returned, not the original value.

Why is nesting `.then()` calls inside other `.then()` calls considered bad practice, even when it reaches the same result as a flat chain?

Nesting recreates the same deeply indented, hard-to-follow shape that promises were introduced to fix in the first place. Returning a promise from a .then() and continuing the chain flat achieves the same sequencing without the nesting, and keeps error handling centralized in one .catch() instead of scattered across nested blocks.

Does an error thrown inside a `.catch()` handler get caught by that same `.catch()`, or does it propagate further?

It propagates further down the chain — a .catch() block, like any .then() handler, produces a new promise, and if the code inside it throws, that new promise rejects and needs its own downstream .catch() to handle it.

How can you make sure an expensive async setup operation only actually runs once, even if multiple parts of the code request it around the same time?

Cache the promise itself the first time it's created, not just its eventual value, and hand that same promise out to every caller. Since a promise remembers its settled value and can have any number of .then() listeners, every caller gets the one real result once it resolves, and the underlying work only runs once.

What does it mean, from the promise API's own perspective, that `.then()`/`.catch()`/`.finally()` callbacks always run as microtasks?

It means those callbacks are never invoked synchronously, even for an already-settled promise — they're always deferred to run after the currently executing code finishes, but before anything else the engine has queued afterward, which is why they reliably run 'soon' but never in the middle of other running code.

Predict the output: two already-resolved promises each get a `.then(console.log)` attached, followed by a plain synchronous `console.log`.

The synchronous log runs first, then the two .then() callbacks run in the order they were scheduled. Both callbacks are deferred as microtasks regardless of the fact that both promises are already resolved — being 'already resolved' doesn't make .then() run synchronously.

You need to run a list of async tasks one at a time, in order, without async/await. How do you chain them dynamically using only promises?

Fold the array into a single chain with reduce: each step's .then() waits for the previous task's promise before starting the next one, seeded with an already-resolved promise as the starting point — building a fully sequential chain from a list of tasks of any length.

Is there any meaningful difference between `Promise.resolve('done')` and manually writing `new Promise(resolve => resolve('done'))`?

Functionally almost none — both give you an already-fulfilled promise with the value 'done'. Promise.resolve() is just a shorter, more direct way to wrap an already-known value or existing thenable, without the ceremony of writing out an executor function.