Overview
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
A Promise is always in one of the following states:
- Pending: The initial state, neither fulfilled nor rejected.
- Fulfilled: The operation completed successfully.
- Rejected: The operation failed.
Unlike “old-style” callbacks, using Promises has the following conventions:
- Callback functions will not be called until the current event loop completes.
- Even if the asynchronous operation completes (successfully or unsuccessfully), callbacks added via
then()
afterward will still be called. - You can add multiple callbacks by calling
then()
multiple times, and they will be executed in the order they were added.
The characteristic feature of Promises is chaining.
Usage
- Promise.all([])
When all Promise instances in the array succeed, it returns an array of success results in the order they were requested. If any Promise fails, it enters the failure callback.
const p1 = new Promise((resolve) => {
resolve(1);
});
const p2 = new Promise((resolve) => {
resolve(1);
});
const p3 = Promise.resolve("ok");
// If all promises succeed, result will be an array of 3 results.
const result = Promise.all([p1, p2, p3]);
// If one fails, the result is the failed promise's value.
2. Promise.allSettled([])
The execution will not fail; it returns an array corresponding to the status of each Promise instance in the input array.
const p1 = Promise.resolve(1);
const p2 = Promise.reject(-1);
Promise.allSettled([p1, p2]).then((res) => {
console.log(res);
});
// Output:
/*
[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: -1 }
]
*/
3. Promise.any([])
If any Promise in the input array fulfills, the returned instance will become fulfilled and return the value of the first fulfilled promise. If all are rejected, it will become rejected.
const p1 = new Promise((resolve, reject) => {
reject(1);
});
const p2 = new Promise((resolve, reject) => {
reject(2);
});
const p3 = Promise.resolve("ok");
Promise.any([p1, p2, p3]).then(
(r) => console.log(r), // Outputs 'ok'
(e) => console.log(e)
);
4. Promise.race([])
As soon as any Promise in the array changes state, the state of the race
method will change accordingly; the value of the first changed Promise will be passed to the race
method’s callback.
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve(10);
}, 3000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error("I encountered an error");
}, 2000);
});
Promise.race([p1, p2]).then(
(v) => console.log(v), // Outputs 10
(e) => console.log(e)
);
Throwing an exception does not change the race
state; it is still determined by p1
.
Advanced Uses
Here are 9 advanced uses that help developers handle asynchronous operations more efficiently and elegantly.
- Concurrency Control
Using Promise.all
allows for parallel execution of multiple Promises, but to control the number of simultaneous requests, you can implement a concurrency control function.
const concurrentPromises = (promises, limit) => {
return new Promise((resolve, reject) => {
let i = 0;
let result = [];
const executor = () => {
if (i >= promises.length) {
return resolve(result);
}
const promise = promises[i++];
Promise.resolve(promise)
.then((value) => {
result.push(value);
if (i < promises.length) {
executor();
} else {
resolve(result);
}
})
.catch(reject);
};
for (let j = 0; j < limit && j < promises.length; j++) {
executor();
}
});
};
2. Promise Timeout
Sometimes, you may want a Promise to automatically reject if it does not resolve within a certain time frame. This can be implemented as follows.
const promiseWithTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("Timeout after " + ms + "ms")), ms)
),
]);
3. Cancelling Promises
Native JavaScript Promises cannot be cancelled, but you can simulate cancellation by introducing controllable interrupt logic.
const cancellablePromise = (promise) => {
let isCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
(value) => (isCanceled ? reject({ isCanceled, value }) : resolve(value)),
(error) => (isCanceled ? reject({ isCanceled, error }) : reject(error))
);
});
return {
promise: wrappedPromise,
cancel() {
isCanceled = true;
},
};
};
4. Sequential Execution of Promise Array
Sometimes you need to execute a series of Promises in order, ensuring that the previous asynchronous operation completes before starting the next.
const sequencePromises = (promises) =>
promises.reduce((prev, next) => prev.then(() => next()), Promise.resolve());
5. Retry Logic for Promises
When a Promise is rejected due to temporary errors, you may want to retry its execution.
const retryPromise = (promiseFn, maxAttempts, interval) => {
return new Promise((resolve, reject) => {
const attempt = (attemptNumber) => {
if (attemptNumber === maxAttempts) {
reject(new Error("Max attempts reached"));
return;
}
promiseFn()
.then(resolve)
.catch(() => {
setTimeout(() => {
attempt(attemptNumber + 1);
}, interval);
});
};
attempt(0);
});
};
6. Ensuring a Promise Resolves Only Once
In some cases, you may want to ensure that a Promise resolves only once, even if resolve
is called multiple times.
const onceResolvedPromise = (executor) => {
let isResolved = false;
return new Promise((resolve, reject) => {
executor((value) => {
if (!isResolved) {
isResolved = true;
resolve(value);
}
}, reject);
});
};
7. Using Promises Instead of Callbacks
Promises provide a more standardized and convenient way to handle asynchronous operations by replacing callback functions.
const callbackToPromise = (fn, ...args) => {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
8. Dynamically Generating a Promise Chain
In some situations, you may need to dynamically create a series of Promise chains based on different conditions.
const tasks = [task1, task2, task3]; // Array of asynchronous tasks
const promiseChain = tasks.reduce((chain, currentTask) => {
return chain.then(currentTask);
}, Promise.resolve());
9. Using Promises to Implement a Simple Asynchronous Lock
In a multi-threaded environment, you can use Promises to implement a simple asynchronous lock, ensuring that only one task can access shared resources at a time.
let lock = Promise.resolve();
const acquireLock = () => {
let release;
const waitLock = new Promise((resolve) => {
release = resolve;
});
const tryAcquireLock = lock.then(() => release);
lock = waitLock;
return tryAcquireLock;
};
This code creates and resolves Promises continuously, implementing a simple FIFO queue to ensure that only one task can access shared resources. The lock
variable represents whether there is a task currently executing, always pointing to the Promise of the task in progress. The acquireLock
function requests permission to execute and creates a new Promise to wait for the current task to finish.
Conclusion
Promises are an indispensable part of modern JavaScript asynchronous programming. Mastering their advanced techniques will greatly enhance development efficiency and code quality. With the various methods outlined above, developers can handle complex asynchronous scenarios more confidently and write more readable, elegant, and robust code.
Leave a Reply