Originally published at Shubho.Dev
Async programming in JavaScript was scary for me. The only async paradigm I was comfortable with was jQuery's $.ajax
. However, I went full vanilla JavaScript for the past 8 years, and when I started working with NodeJS, I had to learn Promises. I haven't dabbled much with third-party libraries like Bluebird. I have the most experience with native Promise.
My main issue with Promise
(or asynchronous paradigm in general) used to be when I wanted to execute statements after the Promise statement began. It took some time to realise that once a Promise statement fires, there is no way to cancel it. Another issue was Promise chaining. This one was a kicker. My earlier functions with Promises always looked like friends of callback hell. After all these years and working on a couple of big projects, I can safely say I love Promises. Even though async/await is the new fad, I still love Promises.
So here is how I use Promises to make my coding life simpler.
Create a Promise skeleton
Whenever I create a new function that returns a Promise, I create the skeleton first. Since the function cannot return anything other than Promise based values, I always wrap all the statements of the function within the Promise.
function sample() {
return new Promise(function(resolve, reject) {
// The function body
});
}
The above sample function wraps its entire statement within the Promise, returning immediately. You can either resolve()
or reject()
the output you want from the body. This way, I never make a mistake of not returning a Promise. It also helps me in creating Promise chains. Whenever in a chain, I realise I need a new function, I create the skeleton name it appropriately and finish the main chain. Then I come back one by one and finish the individual functions.
Promise chains - Points to remember
Promise chaining is tricky. If we are not careful, we can have a new type of callback hell. An example:
function promiseCallback() {
return new Promise((resolve, reject) => {
aNewFunction()
.then((values) => {
someOtherFunction(values)
.then((someOtherValue) => {
// Do something
resolve(someOtherValue);
})
.catch((err1) => {
// Error in inner function
reject(err1);
});
})
.catch((err) => {
// Error in outer function
reject(err);
});
});
}
In the above sample aFunction()
and someOtherFunction()
are two functions returning Promises. If you see carefully, the sequence looks like a callback hell. The inner then and catch the chain, and outer ones are independent. We cannot handle errors in a common catch block, and we need to be careful that the inner functions are always the last line within their outer then()
otherwise we can't control the execution flow.
A better way with chains:
function promiseCallback() {
return new Promise((resolve, reject) => {
aNewFunction()
.then((values) => {
return someOtherFunction(values);
})
.then((someOtherValue) => {
// Do something
resolve(someOtherValue);
})
.catch((err) => {
// Error in outer function
reject(err);
});
});
}
Returns within the then
chain can only have three types:
- Promise - A
then
function in a chain can return a Promise. It's result is passed to the nextthen
. -
Scalar Value - A
then
function in a chain can return a value like a String or a Number. This value is passed to the nextthen
as is and the chain can continue. -
Throw - A
then
function canthrow
an error, which moves the execution to the catch block.
As long as all your returns within a then
follow the above three types, you shouldn't have issues following your Promise chain.
Note
Remember to always resolve()
or reject()
in the last then
or catch
of the chain.
When to create a new Promise function
Within a Promise chain, if there are multiple if-else conditions, and each condition can lead to different Promise results, it is an excellent time to create a new function that returns a Promise. This way, the Promise chain returns a single statement calling the new function.
Handling a scalar value or a Promise function in one step
Assume we have a function which gets the marks attained by a student using his roll number. However, the function either takes a roll number as an input or the name of the student. The marks can be attained from the DB only using the roll number. Here is some pseudo-code.
function getMarks(obj) {
let rollNumberPromise = null;
if ('rollNumber' in obj) {
rollNumberPromise = Promise.resolve(obj.rollNumber);
} else if ('studentName' in obj) {
rollNumberPromise = getRollNumberFromName(obj.studentName);
}
if (!rollNumberPromise) {
reject('Nothing worked');
}
rollNumberPromise
.then((rollNumber) => {
return get_marks_from_db(rollNumber);
})
.then((marks) => {
resolve(marks);
})
.catch((err) => {
reject(err);
});
}
function getRollNumberFromName(studentName) {
return new Promise(function(resolve, reject) {
fn_to_get_roll_number_from_db(studentName)
.then((rollNumber) => {
resolve(rollNumber);
})
.catch((err) => {
reject(err);
});
});
}
function fn_to_get_roll_number_from_db(studentName) {
return new Promise(function(resolve, reject) {
// some code
});
}
function get_marks_from_db(rollNumber) {
return new Promise(function(resolve, reject) {
// some code
});
}
getMarks(obj)
takes an Object as an input. We create a local variable rollNumberPromise
. If the rollNumber is already present, we save the value in the variable using Promise.resolve()
. This creates a Promise which resolves when called with the value. If student’s name is sent, then we save the call to the function getRollNumberFromName(studentName)
to the local variable. Calling rollNumberPromise.then()
returns a rollNumber whether it is received from the DB or sent directly as input to the function. Using it this way ensures that getMarks()
has a single Promise chain, rather than an if-else condition based on whether the input passed was a number or a name.
Invoke a Promise at the end
As mentioned before, once a Promise, once invoked, cannot be cancelled. Any statements which do not depend on the Promise output and which can be carried out independently without an async call should complete before you start a Promise chain in your function. Once a Promise chain begins, any subsequent steps must be within the then
chain. The only exception to this is when you do not care of the Promise value, and you want the Promise to execute in the background while your primary function keeps running.
Conclusion
Promises are difficult. However, with practice and following some rules, it makes working with them a charm. I strictly follow the above rules, and I never go wrong with Promises these days. Find out what you are comfortable with and create your own rules.
Top comments (8)
Hi,
on your template, you seem to be enjoying the verbosity. If not, I recommend:
and since you are using arrow functions, do you always use parentheses? This:
becomes..
Wait, what?!
This:
.. easily becomes this:
Back to your promise template. Here's a much lighter version. You don't need to reject, you throw:
Then on this monstrosity:
Consider changing it into:
It also helps to familiarize with
async/await
constructs.You concluded that 'Promises are difficult'. Please try to understand the main reason they exist (I am not telling you that now, sorry) - you will then see they are not so difficult at all.
Thanks for reading. The verbosity was intentional. The point was to show how to use Promises while writing functions as a beginner. Using arrow functions surely uses less lines of code and I use that in my actual projects. However explaining Promises to my juniors using arrow function was always difficult. I am familiar with
async/await
constructs. I still prefer Promises and use it as much as possible. But my colleagues who came from Java background they preferredasync/await
more 😀Maybe the point of the article was to show how simple it is to wrap everything in a promise, but starting with the original:
And combining with your suggestion (and doing the same with reject):
We can see that the then is just returning the value from the original promise and the catch is also just rejecting with the already rejected value. That means we're just returning the original promise, which can be simplified to:
Which is just the
fn_to_get_roll_number_from_db
function.I'll tell you one thing, that Promise chain example you gave is one of the main reasons prefer other languages over JS. That can easy make some scary ugly monster code !
This isn't really a problem with JS. Like others have shown it can be significantly simplified.
Sure, I must say that at least the new Async Await is cleaner, which is somthing I've actually used much more than Promises, as I spend more time in Go
Seems like callback hell awaiting. Why not use
async
/await
?Thank you for your awesome post, I prefer
async/await
it's much more easier for me.