JavaScript Promises
Understand Promise states, resolution, chaining, rejection propagation, cleanup, combinators, thenable assimilation, and modern Promise APIs.
Intermediate
A Promise is an ECMAScript object representing the eventual result of an operation. The important skill is not memorizing promise methods; it is tracing settlement, chaining, error propagation, and concurrency. This lesson builds that mental model before introducing the Promise combinators and the newer Promise.withResolvers() and Promise.try() APIs.
A Promise has a state, and settlement is one-way
A promise starts pending. It becomes fulfilled with a value or rejected with a reason. Fulfilled and rejected are both settled states. A promise can be resolved to another promise or thenable, which means “resolved” is not always the same thing as “already fulfilled.”
Output
Ready to run.
Quick check
Which pair are the two settled Promise states?
const settled = Promise.resolve(42);
Creating a Promise separates production from observation
The Promise constructor receives an executor. The executor is called during construction, and it receives resolve and reject functions. Calling resolve or reject settles the promise, subject to the promise resolution procedure. The constructor itself is synchronous; the handlers attached with then/catch run later as promise jobs.
Output
Ready to run.
Quick check
What runs synchronously when new Promise(executor) is evaluated?
new Promise(resolve => resolve(1));
Resolution can adopt another Promise
Calling resolve does not necessarily mean “store this value immediately.” If the resolution value is a promise or thenable, the new promise follows that value. This is why Promise resolution is a useful conceptual layer distinct from the final fulfilled/rejected state.
Output
Ready to run.
Quick check
What happens when a Promise is resolved with another Promise?
Promise.resolve(Promise.resolve(3)).then(console.log);
then creates a new Promise and transforms outcomes
then does not mutate the original promise into a new value. It returns a new promise. If the fulfillment handler returns a normal value, the new promise fulfills with that value. If the handler returns a promise or thenable, the new promise adopts it. If the handler throws, the new promise rejects.
Output
Ready to run.
Quick check
What does a then callback returning 10 do to the next Promise in the chain?
Promise.resolve(1).then(() => 10).then(console.log);
Returning a Promise flattens the chain
A then handler can return another Promise. The promise returned by then adopts that result rather than producing a nested Promise that you must manually unwrap. Thenable assimilation extends the same idea to objects with a callable then method.
Output
Ready to run.
Quick check
Why can a then handler return a Promise without creating Promise<Promise<T>> in normal use?
Promise.resolve(1).then(() => Promise.resolve(2));
Thrown errors become rejections in the returned chain
If a fulfillment or rejection handler throws, the Promise returned by then rejects with that thrown value. This gives Promise chains a structured error path: synchronous exceptions inside handlers become asynchronous rejection outcomes for the next rejection handler.
Output
Ready to run.
Quick check
If a then handler throws, what happens to the Promise returned by then?
Promise.resolve().then(() => { throw new Error("x"); }).catch(console.log);
catch is rejection handling through then
catch(onRejected) is equivalent in behavior to attaching a rejection handler with then(undefined, onRejected). It handles rejection from the preceding chain and returns another Promise, so the handler can recover with a value or continue by throwing/returning a rejection.
Output
Ready to run.
Quick check
What can a catch handler return to recover a chain?
Promise.reject("x").catch(() => "fallback");
finally observes completion without replacing the result by default
Promise.prototype.finally runs after the preceding promise is fulfilled or rejected. Its normal completion lets the original outcome continue unchanged. If finally itself throws or returns a rejected Promise, that new failure replaces the earlier outcome.
Output
Ready to run.
Quick check
What normally happens when a finally callback completes normally?
Promise.resolve(7).finally(() => {}).then(console.log);
Promise.resolve normalizes values and thenables
Promise.resolve(x) returns a fulfilled Promise for an ordinary value, but it adopts an existing Promise or compatible thenable. This makes it useful when an API accepts either synchronous values or promise-like results and you want one Promise-based interface.
Output
Ready to run.
Quick check
What is a practical use of Promise.resolve?
Promise.resolve(maybePromise).then(useValue);
Promise.all is for all-or-fail aggregation
Promise.all waits for all input elements to fulfill and then fulfills with an array of values in input order. If any input rejects, the returned Promise rejects with that rejection. “First rejection” describes the aggregate rejection outcome, not cancellation of the other underlying operations.
Output
Ready to run.
Quick check
What order does Promise.all use for its fulfillment array?
Promise.all([slow(), fast()]).then(([slowValue, fastValue]) => {});
Promise.allSettled reports every outcome
Promise.allSettled fulfills after every input has settled. Each result records either status fulfilled with a value or status rejected with a reason. It is appropriate when you need a complete report rather than failing the aggregate at the first rejection.
Output
Ready to run.
Quick check
When is Promise.allSettled useful?
Promise.allSettled(tasks).then(reportAllResults);
Promise.race settles with the first settlement
Promise.race returns a Promise that settles as soon as the first input settles, whether that input fulfills or rejects. It does not itself cancel the slower operations. Cancellation is a separate concern provided by appropriate APIs in the host environment.
Output
Ready to run.
Quick check
What does Promise.race care about?
Promise.race([requestA, requestB]).then(use).catch(handle);
Promise.any waits for the first fulfillment
Promise.any fulfills when the first input fulfills. Rejections are ignored while other inputs remain possible winners. If every input rejects, the aggregate rejects with an AggregateError containing the rejection reasons.
Output
Ready to run.
Quick check
When does Promise.any reject?
Promise.any([primary(), backup()]).catch(handleAllFailures);
withResolvers separates capability creation from executor code
Promise.withResolvers() returns an object containing a new promise plus its associated resolve and reject functions. This can simplify APIs that need to expose or store the settlement controls separately from the code that creates the Promise. It does not make settlement multi-use: the Promise still settles only once.
Output
Ready to run.
Quick check
What does Promise.withResolvers return?
const { promise, resolve, reject } = Promise.withResolvers();
Promise.try converts synchronous calls into Promise outcomes
Promise.try(callback, ...args) calls the callback and turns either a normal return value or a thrown exception into a Promise outcome. It is useful when an API wants one Promise-based path for work that may throw synchronously or may already return a Promise.
Output
Ready to run.
Quick check
What problem does Promise.try address?
Promise.try(() => mayThrow()).catch(handle);
Thenables are assimilated deliberately
A thenable is an object with a callable then property. Promise resolution treats such an object as a promise-like source and adopts its eventual outcome. Do not treat “has a then method” as proof that an object is a native Promise; Promise resolution is the interoperability boundary.
Output
Ready to run.
Quick check
What does Promise.resolve do with a compatible thenable?
Promise.resolve({ then(resolve) { resolve(1); } }).then(console.log);
Choose the combinator from the result you need
Use all when every operation must succeed together. Use allSettled when you need a complete report. Use race when the first settlement determines the result. Use any when the first successful result is enough and all failures matter only if every candidate fails. None of these APIs automatically cancels the underlying operations.
Authoring rules
Trace promise chains by following the Promise returned from each then/catch/finally. Return values fulfill the next Promise; thrown errors reject it; returned thenables are adopted. Keep independent work concurrent when appropriate instead of awaiting each operation unnecessarily. Pick all, allSettled, race, or any based on the outcome contract. Preserve errors rather than converting failures into misleading defaults. Remember that Promise combinators coordinate outcomes; they do not automatically cancel the underlying work.
Practice: design a Promise pipeline
Build a small data pipeline that starts two independent asynchronous operations, combines them with Promise.all, transforms the result with then, handles failures with catch, and performs cleanup in finally. Then rewrite the combination using allSettled and explain what information changes. Try a race and any variant and state exactly what event determines each result.
Specification references
Primary authority: ECMA-262, current ECMAScript specification. Promise objects and algorithms are in the control-abstraction-objects section. Relevant standardized APIs include Promise.resolve/reject, then/catch/finally, all, allSettled, race, any, withResolvers, and try. Current specification: https://tc39.es/ecma262/multipage/ .
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.