Engineering Notes
Home/Languages/JavaScript/Promises
Languages · JavaScript

Promises

A Promise is an object representing the eventual result of an asynchronous operation. It exists in exactly one of three states:

💻 Languages● Advanced⏱ 7 min read🛠️ 4 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What a Promise Is
  • Creating a Promise
  • Consuming a Promise
  • .then() Always Returns a NEW Promise
  • Static Helpers: Promise.resolve / Promise.reject
Part 1The Fundamentals

1What a Promise Is

A Promise is an object representing the eventual result of an asynchronous operation. It exists in exactly one of three states:

StateMeaningCan transition to
pendingoperation not finished yetfulfilled or rejected
fulfilledcompleted successfully, has a value(final — stays fulfilled forever)
rejectedfailed, has a reason (usually an Error)(final — stays rejected forever)

Once a Promise settles (fulfilled or rejected), it never changes state again — this is what makes Promises reliable to reason about, unlike raw callbacks which could theoretically be called multiple times.

Pending transitions to fulfilled via resolve or to rejected via reject, then never changes again
Pending transitions to fulfilled via resolve or to rejected via reject, then never changes again

2Creating a Promise

js
const promise = new Promise((resolve, reject) => {
  const success = Math.random() > 0.3;
  setTimeout(() => {
    if (success) resolve("data loaded");
    else reject(new Error("failed to load"));
  }, 100);
});

The executor function (passed to new Promise) runs immediately and synchronously, but resolve/reject are typically called later, once the async work completes.

3Consuming a Promise

js
promise
  .then(result => console.log("success:", result))
  .catch(error => console.log("failure:", error.message))
  .finally(() => console.log("always runs, success or failure"));
  • .then(onFulfilled, onRejected) — handles both outcomes; the second argument is optional.
  • .catch(onRejected) — shorthand for .then(undefined, onRejected).
  • .finally(fn) — runs regardless of outcome, receives no argument, cannot change the result.

4.then() Always Returns a NEW Promise

js
const p1 = Promise.resolve(5);
const p2 = p1.then(n => n * 2);
console.log(p1 === p2); // false — .then() creates a brand-new Promise

This is the mechanism that makes chaining possible (full detail in the Promise Chaining topic) — each .then() returns a new Promise resolved with whatever its callback returns.

5Static Helpers: Promise.resolve / Promise.reject

js
Promise.resolve(42);              // an already-fulfilled Promise, value 42
Promise.reject(new Error("bad")); // an already-rejected Promise

// Useful for normalizing values that MIGHT already be Promises:
function ensurePromise(value) {
  return Promise.resolve(value); // wraps a plain value; if `value` is already a Promise, returns it as-is
}

6Errors Thrown Inside .then() Become Rejections

js
Promise.resolve(1)
  .then(() => { throw new Error("oops"); })
  .catch(e => console.log("caught:", e.message)); // "caught: oops"

A thrown error inside any .then() callback is automatically converted into a rejected Promise, which the next .catch() (or rejection handler) picks up — no manual reject() call needed inside .then().

7A Promise's Callback Always Runs Asynchronously

js
console.log("1");
Promise.resolve().then(() => console.log("3"));
console.log("2");
// 1, 2, 3 — .then() callbacks NEVER run synchronously, even for an already-resolved Promise

This connects directly to the Microtasks vs Macrotasks topic — .then() callbacks are always scheduled as microtasks, never invoked immediately inline.

Under the HoodThe Internal Slots Behind "Pending/Fulfilled/Rejected"▾

The spec defines a Promise as an object with several internal slots — not regular properties you can access, but real state the engine tracks per Promise instance. Understanding them explains exactly why .then() behaves the way it does.

Internal slotHolds
[[PromiseState]]"pending", "fulfilled", or "rejected"
[[PromiseResult]]the fulfillment value or rejection reason, once settled
[[PromiseFulfillReactions]]a LIST of pending success-handler callbacks, waiting for fulfillment
[[PromiseRejectReactions]]a LIST of pending failure-handler callbacks, waiting for rejection

What .then() actually does to these slots

js
const p = new Promise(resolve => setTimeout(() => resolve(42), 1000));
p.then(value => console.log(value)); // called while p is still PENDING

Calling .then() on a still-pending Promise doesn't run anything yet — it appends a reaction record (your callback, plus the new Promise .then() itself returns) onto [[PromiseFulfillReactions]] and [[PromiseRejectReactions]]. Nothing executes until the Promise actually settles.

What resolve()/reject() actually do

When resolve(value) finally runs, the engine: sets [[PromiseState]] to "fulfilled", sets [[PromiseResult]] to value, then walks the entire [[PromiseFulfillReactions]] list and schedules one microtask per reaction (a "PromiseReactionJob") — this is the exact moment your .then() callbacks actually get queued for execution, not before. This is also why calling .then() on an already-settled Promise still queues a fresh microtask immediately (rather than running synchronously) — the reaction-scheduling code path is identical either way, whether the reaction was already waiting or just got added after the fact.

Why a Promise can only ever settle once

resolve/reject themselves check [[PromiseState]] before doing anything — if it's no longer "pending", the call is a silent no-op. This single guard is the entire mechanism behind "once settled, a Promise never changes state again" — it isn't a convention, it's enforced by a state check on every single resolve/reject invocation, including ones that "lose the race" (see the Promises Part 2 combinator behavior, and Promise.all's internal counter in the Parallel vs Sequential topic).

Part 2Combinators and Patterns

1Promise.all — Wait for Everything, Fail Fast

js
const results = await Promise.all([
  fetch("/api/users"),
  fetch("/api/orders"),
  fetch("/api/products"),
]);
  • Resolves with an array of all results, in the same order as the input, once every Promise fulfills.
  • Rejects immediately with the first rejection reason if ANY Promise rejects — the other Promises keep running in the background, but their results are discarded by Promise.all itself.
  • Use when you need all results and can't proceed with partial data.

2Promise.allSettled — Wait for Everything, Never Fails

js
const results = await Promise.allSettled([
  fetch("/api/users"),
  fetch("/api/might-fail"),
]);
// [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }]

Always resolves (never rejects) once every input Promise has settled, reporting each outcome individually. Use when partial failures are acceptable and you want to know about all of them.

3Promise.race — First to Settle Wins

js
const result = await Promise.race([
  fetch("/api/data"),
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
]);

Settles (fulfilled OR rejected) as soon as the first input Promise settles, in either direction. The classic use case: implementing a timeout by racing a real operation against a timer that rejects.

4Promise.any — First to Fulfill Wins

js
const result = await Promise.any([
  fetch("https://mirror1.example.com/data"),
  fetch("https://mirror2.example.com/data"),
]);

Resolves with the first fulfillment, ignoring rejections — only rejects if all input Promises reject (with an AggregateError containing all reasons). Use for "try several sources, take whichever succeeds first."

5Comparison Table

CombinatorResolves whenRejects when
Promise.allall fulfillany one rejects (immediately)
Promise.allSettledall settle (never rejects)never
Promise.racefirst settles (fulfilled OR rejected)first settles as a rejection
Promise.anyfirst fulfillsall reject

6Turning a Callback API into a Promise (Promisifying)

js
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

This exact wrapping pattern — resolve on success, reject on the error-first callback's error — is how legacy callback-based APIs are adapted to work with async/await (Node also provides util.promisify to automate it for standard error-first functions).

⚠️ Common Pitfalls

  1. Using Promise.all when partial failure is acceptable — one rejection loses all other results; use allSettled instead if you need to know about every outcome.
  2. Forgetting Promise.all runs everything CONCURRENTLY, not sequentially — all input Promises start immediately when the array is created, regardless of await; sequencing must be done explicitly if needed (see the Parallel vs Sequential topic).
  3. Not handling rejection in a "fire and forget" Promise — an unhandled rejected Promise triggers an unhandledRejection warning/crash in Node and browsers; always attach a .catch() or wrap in try/catch with await.
  4. Wrapping an already-async function in new Promise() unnecessarily — if a function already returns a Promise, just return it directly; wrapping it again ("Promise constructor antipattern") adds needless indirection and can swallow errors if done incorrectly.

📋 Quick Reference

text
Promise.all([...])         → all-or-nothing, fails fast
Promise.allSettled([...])   → always resolves, per-item status
Promise.race([...])          → first to settle, either way
Promise.any([...])            → first to succeed, ignores failures until all fail

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What is a promise and its states?core▾

An object representing the eventual result of an async operation: pending, then settled as fulfilled (value) or rejected (reason). It settles once and stays settled; callbacks via then/catch/finally run asynchronously.

Follow-ups interviewers ask
  • Is the executor sync? Yes, the function passed to new Promise runs immediately.
Q2Compare Promise.all, allSettled, race and any.core▾

all: resolves with all values, rejects on the first rejection. allSettled: waits for all, returns statuses. race: settles with the first settled promise. any: resolves with the first fulfillment, rejects with AggregateError if all reject.

Follow-ups interviewers ask
  • Timeout pattern? Promise.race([work, timeout(5000)]) (with AbortController to cancel work).
Q3How do you promisify a callback API?core▾

new Promise((resolve, reject) => fs.readFile(p, (err, data) => err ? reject(err) : resolve(data))) or util.promisify.

Follow-ups interviewers ask
  • What's the explicit-construction anti-pattern? Wrapping existing promises in new Promise unnecessarily.
Q4What happens with unhandled rejections?senior▾

Browsers fire unhandledrejection and log errors; Node (v15+) crashes the process by default. Always attach handlers or await inside try/catch.

Follow-ups interviewers ask
  • Common source? Fire-and-forget async calls without catch.
Q5How do you implement Promise.all yourself?core▾

Return a new promise; track results by index and a counter; resolve when the counter equals the input length; reject on the first error; resolve immediately for an empty array.

Follow-ups interviewers ask
  • Why track by index? Results must keep input order regardless of completion order.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.