Core Concepts and State Transitions
A Promise serves as a placeholder for the eventual result of an asynchronous operation. It operates through a strict lifecycle governed by three states:
pending: The initial phase where the operation is still in progress.fulfilled: The operation completed successfully, yielding a value.rejected: The operation failed, producing an error reason.
State changes are strictly unidirectional. A pending promise transitions to fulfilled via the resolve callback, or to rejected via the reject callback. Once settled, the state becomes immutable and cannnot be altered externally. Furthermore, standard Promise instances cannot be canceled after creation.
const asyncTask = new Promise((resolve, reject) => {
// Asynchronous execution logic
});
function delayExecution(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve('operation finished'), ms);
});
}
delayExecution(200).then((output) => {
console.log(output);
});
Instance Methods
then()
The .then() method attaches handlers for both successful resolution and failure. It accepts two optional arguments: a fulfillment callback and a rejection callback. A critical feature of .then() is that it always returns a new Promise, enabling sequential chaining.
asyncTask.then(
(data) => { /* handle success */ },
(err) => { /* handle failure */ }
);
// Chaining example
const base = Promise.resolve(10);
base
.then((val) => {
console.log(`Phase 1: ${val}`);
return val + 5;
})
.then((updated) => {
console.log(`Phase 2: ${updated}`);
});
// Output: Phase 1: 10 \n Phase 2: 15
catch()
.catch() is syntactic sugar for .then(null, errorHandler). It is considered a best practice to separate success logic in .then() and route all error handling through .catch() for cleaner control flow and reliable error propagation.
base
.then((val) => console.log(`Result: ${val}`))
.catch((err) => console.error(`Caught: ${err.message}`));
Note: Exceptions thrown inside a Promise are contained within the asynchronous context. They do not crash the main execution thread, effectively isolating asynchronous failures.
finally()
The .finally() method registers a callback that executes regardless of whether the Promise fulfills or rejects. It is primarily used for cleanup operations, such as resetting UI states or closing connections.
asyncTask
.then((res) => { /* process data */ })
.catch((err) => { /* manage error */ })
.finally(() => { /* execute cleanup */ });
Static Composition Methods
JavaScript provides several static utilities to manage multiple concurrent asynchronous operations:
Promise.all(): Acepts an iterable of Promises. It fulfills only when every input Promise fulfills, returning an array of results in the original order. If any input rejects, the entire aggregate immediately rejects with that specific reason.Promise.race(): Returns a Promise that settles identically to the first input Promise that settles, whether it fulfills or rejects.Promise.allSettled(): Waits for all input Promises to reach a settled state. It always fulfills with an array of objects describing each outcome ({status: 'fulfilled', value: ...}or{status: 'rejected', reason: ...}), making it ideal for scenarios where individual failures should not halt the batch.Promise.any(): Fulfills as soon as the first input Promise fulfills. It only rejects if every single input rejects, throwing anAggregateError.
Resolution and Rejection Utilities
Promise.resolve() converts values or thenable objects into a fulfilled Promise. Its behavior adapts based on the input:
- Promise instance: Returns the exact same instance without modification.
- Thenable object: Objects with a
.then()method are unwrapped. The method is called immediately to determine the final state. - Primitives or non-thenables: Wrapped into a new fulfilled Promise.
- No arguments: Returns a fulfilled Promise with an
undefinedvalue.
Promise.resolve('sample');
// Functionally identical to: new Promise(res => res('sample'));
Converse, Promise.reject(reason) instantly generates a rejected Promise carrying the provided error reason.
Practical Implementation: Wrapping Network Requests
Promises are frequently used to modernize callback-based APIs. Below is a refactored example demonstrating how to wrap a legacy XMLHttpRequest into a clean, chainable Promise interface:
function fetchJsonData(endpoint) {
return new Promise((fulfill, decline) => {
const request = new XMLHttpRequest();
request.open('GET', endpoint, true);
request.responseType = 'json';
request.setRequestHeader('Accept', 'application/json');
request.onload = function() {
if (request.status >= 200 && request.status < 300) {
fulfill(request.response);
} else {
decline(new Error(`Request failed: ${request.status} ${request.statusText}`));
}
};
request.onerror = function() {
decline(new Error('Network connectivity issue'));
};
request.send();
});
}