Imagine you hand the waiter a ticket after ordering food. That ticket doesn’t give you the meal immediately; it represents a future meal. Later, the restaurant either hands you the food, tells you there was a problem, or sometimes never responds if something goes wrong badly.
A Promise is that ticket in JavaScript. It lets you work with something that will finish later, without blocking everything else. What matters is not just the eventual result, but also how you react when it succeeds, fails, or gets combined with other tickets.
What a Promise Is and Why It Exists
A Promise is an object representing the eventual completion or failure of an async operation. It exists to make async code easier to compose than nested callbacks, and to give a standard way to handle success and failure.
A promise has three states:
pending: still waiting
fulfilled: finished successfully with a value
rejected: finished with a reason/error
A promise can only settle once. After it becomes fulfilled or rejected, it cannot change again. This matters in interviews because many bugs come from assuming a promise can be reused or “re-fired.”
Settling: One Outcome, Forever
A promise starts in pending and then settles exactly once into either fulfilled or rejected. Settling is the moment the future becomes decided; after that, the state is immutable.
This is why promise handlers are safe to register before or after completion. If a promise is already settled when you attach .then() or .catch(), the handler still runs asynchronously with the stored outcome.
Interview trap: people sometimes say a promise is “resolved” when they mean “fulfilled.” In spec terms, resolved can be broader, but for interview purposes the key idea is that once settled, the result is final.
Chaining and Return Values
The most important rule is that .then() returns a new promise. It does not mutate the original one. That new promise is fulfilled with whatever your handler returns.
If the handler returns a plain value, the next promise resolves to that value. If it returns another promise, JavaScript flattens it: the outer chain waits for that inner promise and adopts its final state. This is what makes promise chains compose cleanly.
That means you can think of .then() as a transformation step: success flows forward, and each step can either produce a value, another promise, or throw an error.
Error Handling and Propagation
A thrown error inside .then() becomes a rejection of the promise returned by that .then(). That rejection then travels down the chain until a .catch() handles it.
If a .catch() returns a value, the chain recovers and continues as fulfilled. If it throws again, the rejection continues. This is why .catch() is both a handler and a recovery point.
Interviewers often test whether you know that errors are not “lost” after one .then(). They propagate automatically unless you explicitly handle them.
Combinators: all, allSettled, race, any
Promise combinators coordinate multiple promises, but their failure semantics differ:
Promise.all(): fulfills when all fulfill; rejects immediately on the first rejection.
Promise.allSettled(): waits for all of them and reports every outcome, success or failure.
Promise.race(): settles as soon as the first promise settles, whether fulfilled or rejected.
Promise.any(): fulfills as soon as one fulfills; rejects only if all reject.
The interview trap is assuming they all “wait for everything.” Only all and allSettled wait for every promise; race and any are about the first meaningful outcome, but with opposite success/failure rules.
Promisification: Turning Callbacks into Promises
Promisification means wrapping callback-based APIs in a promise so they can fit into .then() chains or async/await. The wrapper typically resolves on success and rejects on error.
This matters when dealing with older Node-style APIs that use (err, result). A correct wrapper must handle both paths exactly once and pass through the original error reason.
Interviewers like this topic because it tests whether you understand how promises are created from scratch and how they integrate with legacy code.
A chain like fetchUser().then(u => fetchOrders(u.id)) returns a new promise whose value is the orders. If fetchOrders() itself returns a promise, the chain waits for it automatically.
For combinators:
Promise.all([fetchUser(), fetchOrders(7)]) fails if either one fails.
Promise.allSettled([...]) gives both results, even if one rejects.
A classic senior exercise is building a tiny promise that supports then, state, and settling. The goal is not full spec compliance, but to show you understand the core mechanics: store state, queue handlers, settle once, and propagate returned values.
A minimal version needs:
a state like pending/fulfilled/rejected
a stored value or reason
a list of queued handlers
then() returning a new promise
flattening when a handler returns another promise
This exercise is especially useful because it forces you to reason about why promise chaining works at all.