Promises represent eventual completion or failure of async operations with three states (pending, fulfilled, rejected), and async/await provides syntactic sugar for writing promise-based code that reads like synchronous code.
Pending (initial), fulfilled (succeeded with a value), or rejected (failed with a reason). Once settled, the state is permanent.
Syntactic sugar over promises — async functions return promises, await pauses until settlement, and try/catch handles rejections.
all() fails fast on first rejection, allSettled() reports all outcomes, race() settles with the fastest, any() needs at least one success.
await in a loop runs sequentially; Promise.all() runs in parallel. Choosing correctly is a common performance question.
Promise callbacks run as microtasks before setTimeout macrotasks — understanding this execution order is key for predicting async code behavior.
Promises are the foundation of modern asynchronous JavaScript. A Promise is an object representing a value that may not be available yet but will be resolved at some point in the future (or rejected with an error).
A promise is always in one of three states: pending (initial state, operation not yet complete), fulfilled (operation succeeded, promise has a value), or rejected (operation failed, promise has a reason/error). Once a promise is fulfilled or rejected, it is "settled" and its state cannot change again.
.then(onFulfilled, onRejected) handles the result and returns a new promise, enabling chaining. .catch(onRejected) is shorthand for .then(undefined, onRejected) and catches rejections from anywhere earlier in the chain. .finally(onFinally) runs cleanup code regardless of outcome — it receives no arguments and passes through the previous result. A critical pitfall: forgetting to return a value or promise inside .then() breaks the chain — the next .then() receives undefined.
async functions always return a promise. The await keyword pauses execution of the async function until the promise settles, then returns the fulfilled value (or throws the rejection reason). Error handling uses try/catch blocks. Under the hood, async/await compiles to promise chains — it's syntactic sugar, not a new mechanism. await can only be used inside async functions (or at the top level of ES modules).
Four static methods handle multiple promises with different strategies:
Promise.all(iterable): Resolves when all promises fulfill, returning an array of values in order. Rejects immediately if any promise rejects — use when all results are required.Promise.allSettled(iterable): Waits for all promises to settle (fulfill or reject). Returns an array of {status, value} or {status, reason} objects. Never rejects — use when you need all results regardless of individual failures.Promise.race(iterable): Settles with the first promise to settle, whether fulfilled or rejected. Common use case: implementing timeouts by racing a fetch against a delay timer.Promise.any(iterable): Resolves with the first fulfilled promise. Only rejects if all promises reject, throwing an AggregateError containing all rejection reasons. Use when you need at least one success.Using await in a loop executes promises sequentially — each waits for the previous to complete. For parallel execution, start all promises first, then await them: const results = await Promise.all([fetch(url1), fetch(url2)]). This is a common interview question and a real performance consideration.
Promise callbacks (.then, .catch, .finally) are scheduled as microtasks, which run before macrotasks (like setTimeout). This means Promise.resolve().then(() => console.log('A')) always runs before setTimeout(() => console.log('B'), 0), regardless of their order in the code.
Forgetting .catch() or try/catch in async functions leaves rejections unhandled — Node.js terminates on unhandled rejections by default. Creating promises without returning them from .then() creates "fire and forget" operations that swallow errors silently. Calling await on non-promise values works fine (returns the value immediately) but is unnecessary.
Promise.all fails fast on the first rejection. Promise.allSettled never fails — it reports all outcomes. Promise.race settles with whoever finishes first (success or failure). Promise.any needs at least one success. Knowing when to use each is a commonly tested concept.
Fun Fact
Promises were not invented by JavaScript — they originated in 1976 as 'futures' in a paper by Daniel P. Friedman. JavaScript adopted them from the Promises/A+ specification, a community standard that libraries like Bluebird and Q implemented before ES6 made them native in 2015.