Hello There!
Let's learn some advance Javascript promise techniques to write more efficient and reliable JS Code :)
1. Fetching multiple independent resources
consider you have function that fetches a post by ID.
const getPostById = async (id) => {
try {
// make an async call to fetch the post
const post = await loadPost(id)
...
return post;
} catch(err) {
// handle error
}
}
Now if we have to fetch details of multiple posts:
const postIds = [1, 2, 3, 4, ...]
We can do something like this:
const getPosts = (postIds) => {
const posts = [];
for(const id of postIds){
const post = await getPostById(id);
// do processing
...
posts.push(post);
}
return posts;
}
Wait a minute! There is a problem here. The problem is await
keyword will pause the loop until it gets a response from getPostById()
. Fetching each post by Id is an independent operation and result of multiple requests doesn't depend on each other's response. It doesn't make much sense to wait to fetch next post only after previous post has been fetched.
Let's talk how to resolve this issue. What we can do is make multiple requests concurrently and wait for all of them to get fetched or resolved.
Javascript provides two promise APIs to handle multiple requests concurrently:
Promise.all(...)
and Promise.allSettled(...)
Using Promise.all(...)
const getPosts = (postIds) => {
try {
const postPromises = postIds.map(id => getPostById(id));
const posts = await Promise.all(postPromises);
// do processing
...
return posts;
} catch(err) {
// handle error
}
}
Now, good thing is we are not waiting for previous post request to finish to make request for next one instead now concurrent requests will be fired independent of each other and we are waiting until all posts has been fetched. But there is still one issue here. If one of the promises rejects, Promise.all(...)
immediately rejects, causing every other post not to load. We can improvise it by using Promise.allSettled(...)
.
Promise.allSettled(...)
returns a pending promise that resolves when all of the given promises have been settled either resolved or rejected. This behaviour is very useful to track multiple tasks that are not dependent on one another to complete.
const getPosts = (postIds) => {
const postPromises = postIds.map(id => getPostById(id));
const posts = await Promise.allSettled(postPromises);
// outcome of each promise has a status property.
// If success, it will have value property
// If fails, it will have reason property
return posts.reduce((result, post) => {
if(post.status === 'fulfilled') {
result.successfullyFetchedPosts.push(post.value)
} else {
result.failedPosts.push(post.reason)
}
return result;
}, {
successfullyFetchedPosts: [],
failedPosts: [],
})
}
// using this function
const {
successfullyFetchedPosts,
failedPosts
} = await getPosts([...]);
...
Promise returned by Promise.allSettled(...)
will almost always be fulfilled. The promise will only reject if we pass a value that is not iterable.
2. Avoiding single point of failure using Promise.any(...)
Sometimes, we have to fetch some critical resource like financial market data from external APIs. If the API is down, the app will stop working. The Promise.any(...)
is extremely useful in this regard. It enables us to request data from multiple sources ( APIs) and use the result of the first successful promise.
Promise.any(...)
returns a pending promise that resolves asynchronously as soon as one of the promises in the given iterable fulfils.
const promises = [
Promise.reject(),
Promise.resolve(5),
Promise.reject(),
];
Promise.any(promises).then(console.log) // 5
Consider we have three APIs to fetch a resource. We can use Promise.any(...)
like this:
const apis = [
'https://api1/resource/10',
'https://api2/resource/10',
'https://api3/resource/10'
];
const fetchData = async api => {
const response = await fetch(api);
return response.ok ? response.json() : Promise.reject('some error');
}
const getResource = () => Promise.any(
apis.map(api => fetchData(api))
);
getResource().then(response => {
// process response
})
Promise.any(...)
allows you to improve the performance of critical applications by using the data from the API that responds first. Also it allows you to improve reliability of the application as even if one of the APIs fails, it will continue working as expected. Promise.any(...)
will only reject when all the promises passed as the argument reject.
3. Enforcing a time limit for async operations using Promise.race(...)
Suppose we are fetching some resource from an external API. User Interface will be in the loading state until we get response from the API. Sometimes, APIs take a lot of time to give response back to the client and user will be waiting for it , looking at the loading spinner like forever. A better user experience would be to timeout the request after a given number of milliseconds and show error in the UI like request time out. We can easily do this using Promise.race(...)
.
Promise.race(...)
is somewhat similar to Promise.any(...) as both get settled whenever first promise in the iterable settles
.
Promise.race(...)
settles as soon as one of the promises rejects. .
Promise.any(...)
resolves as soon as one of the promises fulfils.
Promise.race(...)
rejects if the first promise that settles is rejected while Promise.any(...)
rejects when all the given promises reject.
let's implement timeout of the request:
const getData = async () => {
const TIMEOUT_MS = 2000; // IN MILLISECONDS
const request = fetch(API_URL); // actual request
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request time out!')), TIMEOUT_MS)); // rejects after 2000 milliseconds
return Promise.race([request, timeout];
}
4. Batching async requests using Promise.race(...)
An interesting use case of Promise.race(...)
is to batch async request. Here is the simple implementation:
/**
*
* @param {{limit: number, concurrentBatches: number}} batchOptions
*/
const performRequestBatching = async batchOptions => {
const query = {
offset: 0,
limit: batchOptions.limit
};
let batch = [];
let promises = [];
do {
batch = await model.findAll(query);
query.offset += batchOptions.limit;
if (batch.length) {
const promise = performLongRequestForBatch(batch).then(() => {
// remove the promise from promises list once it is resolved
promises = promises.filter(p => p !== promise);
});
promises.push(promise);
// if promises length is greater than provided max concurrent batches
if (promise.length >= batchOptions.concurrentBatches) {
// then wait for any promise to get resolved
await Promise.race(promises);
}
}
} while (batch.length)
// wait for remaining batches to finish
return Promise.all(promises);
}
// using batching
batchRequest({
limit: 100,
concurrentBatches: 5,
})
Conclusion
Hurray! That's it. We have learnt multiple patterns related to Javascript promise api like handling multiple request efficiently using Promise.all
and Promise.allSettled
, avoiding SPOF using Promise.any
, timing out async request for better user experience and batching multiple requests using Promise.race
.
Please like the post if you have learnt something new :). Also Feel free to point-out or provide suggestions in the comment section if there is some mistake in the post.
Happy Coding !
See you!
Top comments (1)
Thanks for sharing @lukeshiru . Liked your approach of making the function generic .
Some comments may only be visible to logged-in visitors. Sign in to view all comments.