One way to get rid of your crush or JavaScript’s callback hell is to make promises. Now there are dozens of quotes revolving about broken promises in love; but thankfully JavaScript handles promises elegantly. Let us dig more into this topic and understand how difficult or easy it is to make promises in love or JavaScript.
For quick read. Checkout this amazing album!
Love doesn't need promises but JavaScript does. We have seen previously what hell callback creates when we talk about async operations; checkout the article Why Your Crush And Javascript Does Not Callback Anymore if not read already. So let us begin from the previous article’s cliffhanger, and watch Promises in action. In this article we will pick all the previous examples and understand how easily Promise can take all the responsibilities of a callback.
Google defines promise as, to assure someone that one will definitely do something or that something will happen; JavaScript Promise is no different, it is an assurance of completion of an async operation, either completed or failed but surely executed.
Async operations require extra care and pampering as compared to the sync ones, since they work on different time dimensions, so to handle them we must identify what should be done if it is completed or failed.
In terms of JavaScript a promise object either gets resolved (fulfilled) or rejected, nothing in between. Let us create the simplest form of promise and chat about it.
const isEven = (number) => {
return new Promise((resolve, reject) => {
// super complex math problem
if (number % 2 === 0) {
resolve();
} else {
reject();
}
});
};
isEven(4)
.then(
() => console.log("Yay! it's even"),
() => console.log("Meh! it's odd")
);
The same example we have seen in the previous callback article where we have created a callback function to handle the even or odd results. A promise object takes a callback with resolve and reject functions which can be used by the callee to take the success or failed paths. The callee can make use of then() method which also takes 2 callbacks for resolve and reject.
Additionally along with then() method a catch() method can also be chained and instead of using 2 callbacks within then() method we can make use of catch() method to handle the rejection.
isEven(4)
.then(() => console.log("Yay! it's even"))
.catch(() => console.log("Meh! it's odd"));
The then() and catch() approach is most common as it makes code more readable and understandable. In case if the on reject callback of then() method and catch() method is provided then the on reject callback of then() method will be called.
Since resolve and reject are nothing but methods, we can pass parameters to them and can obtain the supplied values in then() and catch() methods respectively. Consider the below factorial example which is seen already with callbacks.
const factorial = (number) => {
return new Promise((resolve, reject) => {
if (typeof number === 'number') {
let fact = 1;
for (let i = 1; i <= number; i++) {
fact *= i;
}
resolve(fact);
} else {
reject('The number provided must be of type number');
}
});
};
factorial(3)
.then(result => console.log('Factorial:', result))
.catch(error => console.log('Error:', error));
Here we are simply providing the factorial or the error message in case if the method gets a non numeric value.
Let us create an artificial asynchronous task and handle it using promise just like we did with callbacks.
We will make use of the same data source shown below acting as a database for us and a function named fetchUserById which will fetch users from the db but takes time to execute.
const db = {
users: [
{ id: 1, name: 'Allen' },
{ id: 2, name: 'John' },
{ id: 3, name: 'Martin' },
],
posts: [
{ id: 101, userId: 1, title: 'Nisi sint cillum officia laborum consequat labore.' },
{ id: 102, userId: 1, title: 'Ipsum ea fugiat velit do.' },
{ id: 103, userId: 2, title: 'Qui ullamco veniam non sit mollit.' },
{ id: 104, userId: 3, title: 'Laboris qui officia anim proident Lorem esse aliquip.' },
],
comments: [
{ id: 11, postId: 101, userId: 2, comment: 'Nulla tempor nisi dolor velit id qui culpa et tempor eiusmod sint.' },
{ id: 12, postId: 101, userId: 1, comment: 'Reprehenderit est cupidatat magna eu anim.' },
{ id: 13, postId: 102, userId: 3, comment: 'Sint adipisicing sint ad cillum ipsum aute voluptate ea fugiat nostrud ut.' },
{ id: 14, postId: 103, userId: 1, comment: 'Mollit id ipsum sunt laborum duis.' },
{ id: 15, postId: 101, userId: 3, comment: 'Exercitation ipsum pariatur sit Lorem deserunt occaecat.' },
]
};
Here the below fetchUserById() method will provide the user based on the id. The function will complete after 2 seconds and later calls the resolve or reject methods, either providing error or result.
const db = require('./db')
const fetchUserById = (id) => {
return new Promise((resolve, reject) => {
setTimeout(_ => {
const results = db.users.find(i => i.id === id);
results ?
resolve(results) :
reject('Not found');
}, 2000);
});
};
fetchUserById(1)
.then(user => console.log('User:', user))
.catch(err => console.log('Error:', err))
.finally(() => console.log('I am Inevitable'));
Notice we have added a finally() method to the chain of then and catch. Just like Thanos finally() method is inevitable, means either the promise gets resolved or rejected, the finally() method will be executed anyhow.
When you have a bundle of promises to deal with, the JavaScript promise object comes up with a bunch of useful methods, these methods are fun to understand and have different purposes to tackle different scenarios. They are as follows:
To understand their specialities we will consider 3 scenarios for a number of promise objects,
Consider the below code, where we are creating a bunch of promises, and as per scenario we will do the tweak. This will serve as a base for understanding the above mentioned methods.
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.all()
This method waits until all promises are resolved and it goes to catch if any of them gets rejected.
First scenario - all of our promises get resolved:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.all(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
// result: [ 'value 1', 'value 2', 'value 3' ]
Second scenario - some of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.all(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
// error: reason 2
Third scenario - all of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 3'), 2000));
const promises = [p1, p2, p3];
Promise.all(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
// error: reason 2
The behaviour for second and third scenarios are the same.
Promise.allSettled()
This method waits until all promises are resolved or rejected. It shows the status of each promise with an object. Format: { status: 'fulfilled', value: 'value 1' }.
First scenario - all of our promises get resolved:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.allSettled(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
/* result [
{ status: 'fulfilled', value: 'value 1' },
{ status: 'fulfilled', value: 'value 2' },
{ status: 'fulfilled', value: 'value 3' }
] */
Second scenario - some of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.allSettled(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
/* result [
{ status: 'fulfilled', value: 'value 1' },
{ status: 'rejected', reason: 'reason 2' },
{ status: 'fulfilled', value: 'value 3' }
] */
Third scenario - all of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 3'), 2000));
const promises = [p1, p2, p3];
Promise.allSettled(promises)
.then(result => console.log('result', result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
/* result [
{ status: 'rejected', reason: 'reason 1' },
{ status: 'rejected', reason: 'reason 2' },
{ status: 'rejected', reason: 'reason 3' }
] */
It simply waits either resolved or rejected and shows an overall summary with the resolved and rejected values.
Promise.race()
This method waits until any of the promises are either resolved or rejected. It selects the promise who wins the race irrespective of resolved or rejected outcomes.
First scenario - all of our promises get resolved:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.race(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 500 milliseconds]
// result: value 2
Second scenario - some of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.race(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 500 milliseconds]
// error: reason 2
Third scenario - all of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 3'), 2000));
const promises = [p1, p2, p3];
Promise.race(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 500 milliseconds]
// error: reason 2
Promise.any()
This method waits until any of the promises are resolved. It goes to catch if all of the promises are rejected. This one is simply opposite of the Promise.all() method. This method is available in Node version above 15.0.
First scenario - all of our promises get resolved:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.any(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 500 milliseconds]
// result: value 2
Second scenario - some of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => resolve('value 3'), 2000));
const promises = [p1, p2, p3];
Promise.any(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 2000 milliseconds]
// result: value 3
Third scenario - all of our promises get rejected:
const p1 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 1'), 3000));
const p2 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 2'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(_ => reject('reason 3'), 2000));
const promises = [p1, p2, p3];
Promise.any(promises)
.then(result => console.log(result))
.catch(error => console.log('error', error));
// output: [waits for 5500 milliseconds]
// error:: [AggregateError: All promises were rejected]
Promise.resolve() and Promise.reject()
If you simply want to execute the resolve or reject part of the promise you can simply make use of Promise.resolve() or Promise.reject() methods. These are mostly used in case of debugging.
Promise.resolve('resolved')
.then(result => console.log('result:', result))
.catch(error => console.log('error:', error));
Promise.reject('reject')
.then(result => console.log('result:', result))
.catch(error => console.log('error:', error));
This is the cure to callback hell, the best part of promise is that it can save you from falling into a pyramid of doom. When you have a scenario where you want to perform more and more async operations after completing an async operation, you can chain the promises. Let us understand this with an example.
const marks = [76, 64, 56, 87, 48];
Promise.resolve(marks)
.then(marks => marks.reduce((acc, crr) => acc + crr, 0))
.then(obtained => obtained / (marks.length * 100))
.then(result => result * 100)
.then(percentage => console.log(percentage));
Here we are using the promise resolve method directly without creating a promise object.; this simply shows the resolved part of the promise. We want to obtain overall percentage of marks, for which we are first calculating obtained marks by simply reducing them and doing a sum of all marks, then we divide the obtained marks by overall total marks which is 100 for per subject marks, then we multiply the result by 100 to obtain the final percentage.
The above example explains how we can call the then() method followed by another then() method. So one needs to return a value or another async method from the current then() method which will act as a resolution or rejection for the next chained then() or catch() method. We will see some async operation chaining later in the next section.
Let us recreate the scenario we have seen in the previous article regarding callback hell. We will consider the same db object, which will act as a data source for us. Considering a scenario where we have some REST API endpoints which provide async data for users, posts and comments; for each of them we have separate methods which will bring data to us. But what if we need a user object which should consist of their id, name, posts and each post should contain their respective comments if any. Such requirements can be fulfilled once all the data are in place.
Let us first create the async methods which will provide data to user, post and comment entities. These methods are the same as we have seen in the previous callback article, only instead of using callbacks they are made with Promises.
const db = require('./db')
const fetchUserById = (id) => {
return new Promise((resolve, reject) => {
setTimeout(_ => {
const results = db.users.find(i => i.id === id);
results ?
resolve(results) :
reject('Not found');
}, 500);
});
};
const fetchPostsByUserId = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(_ => {
const results = db.posts.filter(i => i.userId === userId);
results.length ?
resolve(results) :
reject('Not found');
}, 500);
});
};
const fetchCommentsByPostId = (postId) => {
return new Promise((resolve, reject) => {
setTimeout(_ => {
const results = db.comments.filter(i => i.postId === postId);
results.length ?
resolve(results) :
reject('Not found');
}, 500);
});
};
Now let us duplicate the callback hell example with help of promise:
let result;
fetchUserById(1)
.then(user => {
result = user;
fetchPostsByUserId(user.id)
.then(posts => {
result.posts = posts;
Promise.all(result.posts.map(i => fetchCommentsByPostId(i.id)))
.then(comments => {
result.posts.forEach(post => post.comments = comments.flat().filter(i => i.postId === post.id));
console.log('User:', result);
})
.catch(error => console.log('Error:', error));
})
.catch(error => console.log('Error:', error));;
})
.catch(error => console.log('Error:', error));
The comments are associated with post ids, so in order to form a link between post and comment we need to get comments by post ids, so we are in a need to iterate each post and bring their comments accordingly. But since things are async over here we made a promise array using array’s map function (checkout this post if you wanna know more about array functions) and passed it as an input to the previously seen promise all() method. So once all promises are resolved we can now form a user object having posts related to them with their associated comments. But the comments which we will receive from the all() method constitute arrays of comment arrays, so to flatten them we make use of array’s flat method and then simply filter each of these comments by post id and assign it to respective user’s post property.
Keeping the business logic aside, here we are still following the traditional way of calling then() after then() in a nested fashion; we are actually wasting the potential of JavaScript promises. The above code will work as per the requirement but in an ugly way.
Checkout the below code where we make use of awesome promise chaining technique, which makes life and code much simpler to read and maintain.
let result;
fetchUserById(1)
.then(user => {
result = user;
return fetchPostsByUserId(user.id);
})
.then(posts => {
result.posts = posts;
return Promise.all(result.posts.map(i => fetchCommentsByPostId(i.id)));
})
.then(comments => {
result.posts.forEach(post => post.comments = comments.flat().filter(i => i.postId === post.id));
return result;
})
.then(result => console.log('User:', result))
.catch(error => console.log('Error:', error));
Each promise resolution is chained by another promise, and a final catch if any of these promises fails in between, obviously we can add catch() in between too if we don't wanna share a common catch for all.
In the above example we have tried to mimic some real world scenarios which we most commonly face while developing some gold applications. The promise chaining ninja technique brought a revolution to many of the coding styles we had previously.
But this doesn't stop us, the greed of making an async code to look like a sync one originated 2 new keywords async/await. We will check about async/await in near future articles and compare them with promises side by side.
December 31, 2020
October 19, 2020
March 02, 2022