What are Promises and how do they work in JavaScript?

Intermediate12 min interview
Skills tested:
Promise states: pending, fulfilled, rejectedPromise chaining with .then and .catchPromise.all, Promise.allSettled, Promise.race, Promise.anyError propagation through a Promise chainConverting callback-based APIs to Promises

Advertisement

🧩 Scenario

In a real codebase, Promises are the foundation of every network request, file operation, database query, and timer-based API. Understanding Promises is a prerequisite for using async/await correctly because async/await is syntactic sugar that translates directly into Promise operations at the runtime level. The most common production issues involve incorrect chaining, unhandled rejections, and misuse of Promise.all when failures in one operation should not block others.

Architecture Walkthrough

Promise States and Transitions

A Promise is an object that represents the eventual result of an asynchronous operation. It exists in one of three states: pending (the operation is in progress), fulfilled (the operation completed with a value), or rejected (the operation failed with a reason). Once a Promise transitions from pending to either fulfilled or rejected, it is settled and cannot change state again.

The executor function passed to new Promise() runs synchronously when the Promise is created. Inside the executor, calling resolve(value) transitions the Promise to fulfilled and calling reject(reason) transitions it to rejected. The callbacks registered with .then(), .catch(), and .finally() always execute asynchronously in the microtask queue, even if the Promise is already settled.

Chaining and Error Propagation

.then() returns a new Promise, which enables chaining. The value returned by a .then callback becomes the resolved value of the next Promise in the chain. If a .then callback returns a Promise instead of a plain value, the chain waits for that inner Promise to settle before continuing.

Errors propagate down the chain automatically. If any .then callback throws or if a Promise rejects, the error skips all subsequent .then callbacks until it reaches a .catch handler. A .catch at the end of a chain handles errors from any point earlier in the chain, which is the idiomatic Promise error handling pattern.

Promise Combinators

Promise.all accepts an array of Promises and returns a single Promise that resolves when all input Promises resolve, with an array of their values. If any input Promise rejects, Promise.all immediately rejects with that reason and ignores all other Promises. This short-circuit behavior makes Promise.all appropriate only when all operations must succeed.

Promise.allSettled waits for all Promises regardless of whether they fulfill or reject. It resolves with an array of result objects, each containing a status field ("fulfilled" or "rejected") and either a value or reason. Use Promise.allSettled when partial failures are acceptable and you need the results of all operations. Promise.race resolves or rejects with the first settled Promise. Promise.any resolves with the first fulfilled Promise and only rejects if all reject.


Key Code Explained

// Creating a Promise
const fetchUser = (id) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) resolve({ id, name: 'Ghazi' });
      else reject(new Error('Invalid ID'));
    }, 500);
  });

// Chaining .then and .catch
fetchUser(1)
  .then((user) => {
    console.log(user.name); // 'Ghazi'
    return fetchUser(2); // returning a Promise: chain waits for this
  })
  .then((user2) => console.log(user2.name))
  .catch((err) => console.error(err.message)); // catches any rejection in the chain

// Promise.all: all must succeed
Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)])
  .then(([u1, u2, u3]) => console.log(u1, u2, u3))
  .catch((err) => console.error('At least one failed:', err.message));

// Promise.allSettled: partial failures are fine
Promise.allSettled([fetchUser(1), fetchUser(-1), fetchUser(3)]).then(
  (results) => {
    results.forEach((r) => {
      if (r.status === 'fulfilled') console.log('OK:', r.value.name);
      else console.log('Failed:', r.reason.message);
    });
  },
);

// Promisifying a callback-based API
function readFileAsync(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf8', (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

The .then chain example demonstrates the key behavior: returning a Promise from inside .then causes the chain to wait for it. This is different from returning a plain value. Forgetting to return (or accidentally returning a plain value when you intended to return a Promise) is one of the most common chaining bugs.


Tradeoffs

CombinatorBehavior on rejectionUse when
Promise.allShort-circuits on first rejectionAll operations must succeed
Promise.allSettledWaits for all, collects statusesPartial failure is acceptable
Promise.raceResolves or rejects with first to settleTimeout pattern or fastest-wins logic
Promise.anyResolves with first to fulfillRedundant requests, first success wins

What Interviewers Actually Check

  • Whether you can describe the three states and the one-way transition between them
  • Whether you know that .then returns a new Promise and enables chaining
  • Whether you know that errors propagate down the chain to the nearest .catch
  • Whether you understand the difference between Promise.all and Promise.allSettled
  • Whether you know that the executor function runs synchronously but .then callbacks are always async

Follow-Up Questions

  1. What is an "unhandled rejection" and how does Node.js handle it by default in recent versions?
  2. How would you implement Promise.all yourself using just new Promise and a counter?
  3. What is the relationship between async/await and the Promise microtask queue?
  4. If you chain .then(() => somePromise()).then(result => ...) and somePromise rejects after 2 seconds, what happens to the outer chain?
  5. A teammate says that wrapping every API call in new Promise() is fine. When is this an anti-pattern?

Common Candidate Mistakes

  • Forgetting to return a Promise from inside a .then callback, which breaks the chain and causes the outer Promise to resolve with undefined
  • Not attaching a .catch handler and ignoring unhandled rejection warnings that indicate silent failures
  • Thinking Promise.all handles individual rejections gracefully when it short-circuits on the first one
  • Wrapping code that already returns a Promise inside new Promise() unnecessarily (the explicit Promise constructor anti-pattern)
  • Confusing Promise.all and Promise.allSettled, using the wrong one when partial failures are expected

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you describe the three Promise states and what transitions exist between them?
  • Can you chain .then calls correctly, including returning a new Promise from inside .then?
  • Can you explain the difference between Promise.all and Promise.allSettled?
  • Can you write a promisify wrapper that converts a Node.js error-first callback into a Promise?
  • Can you trace error propagation through a multi-step Promise chain?

Summary

A Promise is an object representing the eventual fulfillment or rejection of an asynchronous operation. It starts in the pending state and transitions to fulfilled or rejected exactly once. Callbacks registered with .then receive the resolved value; those registered with .catch receive the rejection reason. Both always execute asynchronously in the microtask queue.

Chaining Promises requires returning a value or a new Promise from each .then callback. Returning a Promise causes the chain to wait for it to settle before continuing. Errors propagate automatically through the chain to the nearest .catch, which simplifies error handling for multi-step async sequences compared to nested callbacks.

For multiple concurrent operations, Promise.all is appropriate when all must succeed and one failure should abort the group. Promise.allSettled is appropriate when partial failures are acceptable and you need the result of every operation regardless of outcome.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Do Promises execute immediately?

Yes, the executor function inside new Promise() runs synchronously when the Promise is created. The resolution callbacks (.then) run asynchronously.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement