Learn how to create custom promise utilities in JavaScript and TypeScript to enhance asynchronous programming. Explore examples like promiseTimeout, retryPromise, and debouncePromise, and discover best practices for handling edge cases, parameter validation, and performance optimization.
In the realm of modern JavaScript and TypeScript development, promises have become a cornerstone for handling asynchronous operations. While the built-in capabilities of promises are robust, there are scenarios where custom utility functions can significantly enhance their functionality. This section delves into creating custom promise utilities that extend the capabilities of promises, making your asynchronous code more powerful and expressive.
Promises are versatile, but they aren’t a one-size-fits-all solution. Custom promise utilities can help address specific needs such as timeouts, retries, and debouncing, which are common in real-world applications. By developing these utilities, you can:
Let’s explore some practical examples of custom promise utilities and how they can be implemented.
promiseTimeout
A common requirement in asynchronous programming is to ensure that a promise settles within a specific timeframe. If it doesn’t, the operation should fail gracefully. This is where a promiseTimeout
utility can be invaluable.
/**
* Wraps a promise with a timeout. If the promise doesn't settle within the specified time, it is rejected.
* @param {Promise} promise - The promise to wrap.
* @param {number} ms - The timeout in milliseconds.
* @returns {Promise} - A promise that resolves or rejects based on the original promise or the timeout.
*/
function promiseTimeout(promise, ms) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error('Promise timed out'));
}, ms);
});
return Promise.race([promise, timeoutPromise])
.finally(() => clearTimeout(timeoutId));
}
// Usage example
const fetchData = new Promise((resolve) => setTimeout(() => resolve('Data fetched'), 1000));
promiseTimeout(fetchData, 500)
.then(console.log)
.catch(console.error); // Outputs: Error: Promise timed out
Key Points:
Promise.race()
is used to race the original promise against a timeout promise.clearTimeout()
ensures that the timeout is cleared once the promise settles, preventing memory leaks.retryPromise
UtilityNetwork requests and other asynchronous operations can fail due to transient errors. A retryPromise
utility can automatically retry a promise-based operation a specified number of times before giving up.
/**
* Retries a promise-based function until it succeeds or the maximum number of attempts is reached.
* @param {Function} fn - The function returning a promise to retry.
* @param {number} retries - The maximum number of retries.
* @param {number} delay - The delay between retries in milliseconds.
* @returns {Promise} - A promise that resolves or rejects based on the operation success or failure.
*/
function retryPromise(fn, retries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
fn().then(resolve).catch((error) => {
if (n === 0) {
reject(error);
} else {
setTimeout(() => attempt(n - 1), delay);
}
});
};
attempt(retries);
});
}
// Usage example
const unreliableFetch = () => new Promise((resolve, reject) => Math.random() > 0.5 ? resolve('Success') : reject('Failure'));
retryPromise(unreliableFetch, 5, 500)
.then(console.log)
.catch(console.error);
Key Points:
debouncePromise
FunctionDebouncing is a technique used to limit how often a function can be called. This is particularly useful for operations like API requests triggered by user input.
/**
* Creates a debounced version of a function that returns a promise.
* @param {Function} fn - The function to debounce.
* @param {number} delay - The debounce delay in milliseconds.
* @returns {Function} - A debounced function that returns a promise.
*/
function debouncePromise(fn, delay) {
let timeoutId;
return function (...args) {
return new Promise((resolve, reject) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn(...args).then(resolve).catch(reject);
}, delay);
});
};
}
// Usage example
const fetchData = () => new Promise((resolve) => setTimeout(() => resolve('Data fetched'), 300));
const debouncedFetchData = debouncePromise(fetchData, 500);
debouncedFetchData().then(console.log); // Outputs: Data fetched (after 500ms delay)
Key Points:
While promises don’t natively support cancellation, custom utilities can provide a mechanism to simulate cancellation. This involves using a flag or token to signal cancellation and handling it within the promise logic.
/**
* Creates a cancellable promise.
* @param {Function} fn - The function that performs the asynchronous operation.
* @returns {Object} - An object containing the promise and a cancel method.
*/
function cancellablePromise(fn) {
let isCancelled = false;
const promise = new Promise((resolve, reject) => {
fn().then((result) => {
if (!isCancelled) {
resolve(result);
}
}).catch((error) => {
if (!isCancelled) {
reject(error);
}
});
});
return {
promise,
cancel() {
isCancelled = true;
}
};
}
// Usage example
const { promise, cancel } = cancellablePromise(() => new Promise((resolve) => setTimeout(() => resolve('Done'), 1000)));
setTimeout(cancel, 500); // Cancels the promise after 500ms
promise.then(console.log).catch(console.error); // Promise is cancelled, so neither log nor error is called
Key Points:
When developing custom promise utilities, it’s crucial to handle edge cases and errors effectively. Consider scenarios such as:
A deep understanding of promise mechanics is essential when designing custom utilities. This includes knowledge of:
When designing promise utilities, avoid common anti-patterns such as:
Custom promise utilities should evolve based on real-world usage and feedback. Encourage continuous learning by:
Creating custom promise utilities in JavaScript and TypeScript can greatly enhance your ability to manage asynchronous operations. By understanding the mechanics of promises and following best practices, you can develop utilities that are robust, reusable, and easy to integrate into your projects. Whether it’s implementing timeouts, retries, or debouncing, these utilities can make your code more expressive and reliable. Remember to document your utilities thoroughly, test them rigorously, and share them with the community to contribute to the broader ecosystem of asynchronous programming.