Promises rule JavaScript. Even nowadays, with introduction of async/await, they are still an obligatory knowledge for any JS developer.
But JavaScript differs in how it deals with asynchronicity from other programming languages. Because of that, even developers with a lots of experience can sometimes fall into its traps. I have personally seen great Python or Java programmers making very silly mistakes when coding for Node.js or browsers.
Promises in JavaScript have many subtleties which one has to be aware of in order to avoid those mistakes. Some of them will be purely stylistic, but many can introduce actual, difficult to track errors. Because of that, I have decided to compile a short list of the three most common mistakes I have seen developers do, when programming with Promises.
Wrapping everything in a Promise constructor
This first mistake is one of the most obvious, and yet I have seen developers do it surprisingly often.
When you first learn about Promises, you read about a Promise constructor, which can be used to create new Promises.
Perhaps because people often start learning by wrapping some browser APIs (like setTimeout
) in the Promise constructor, it gets ingrained in their minds that the only way to create a Promise is to use the constructor.
So as a result they often end up with a code like this:
const createdPromise = new Promise(resolve => {
somePreviousPromise.then(result => {
// do something with the result
resolve(result);
});
});
You can see that in order to do something with the result
from somePreviousPromise
someone used then
, but later decided to wrap it again in a Promise constructor, in order to store that computation in the createdPromise
variable, presumably in order to do some more manipulations on that Promise later.
This is of course unnecessary. The whole point of then
method is that it itself returns a Promise, that represents executing somePreviousPromise
and then executing a callback passed to the then
as an argument, after somePreviousPromise
gets resolved with a value.
So the previous snippet is roughly equivalent to:
const createdPromise = somePreviousPromise.then(result => {
// do something with result
return result;
});
Much nicer, isn’t it?
But why I wrote that it is only roughly equivalent? Where is the difference?
It might be hard to spot for the untrained eye, but in fact there is a massive difference in terms of error handling, much more important than the ugly verbosity of the first snippet.
Let’s say that somePreviousPromise
fails for any reason and throws an error. Perhaps that Promise was making a HTTP request underneath and an API responded with a 500 error.
It turns out that in the previous snippet, where we wrap a Promise into another Promise, we have no way to catch that error at all. In order to fix that, we would have to introduce following changes:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// do something with the result
resolve(result);
}, reject);
});
We simply added a reject
argument to the callback function and then used it by passing it as a second parameter to the then
method. It’s very important to remember that then
method accepts second, optional parameter for error handling.
Now if somePreviousPromise
fails for any reason, reject
function will get called and we will be able to handle the error on createdPromise
as we would do normally.
So does this solve all of the problems? Unfortunately no.
We handled the errors that can occur in the somePreviousPromise
itself, but we still don’t control what happens within the function passed to the then
method as a first argument. The code that gets executed in the place where we have put the // do something with the result
comment might have some errors. If the code in this place throws any kind of error, it will not be caught by the reject
function placed as a second parameter of the then
method.
That’s because error handling function passed as a second argument to then
only reacts to errors that happen earlier in our method chain.
Therefore, the proper (and final) fix will look like this:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// do something with the result
resolve(result);
}).catch(reject);
});
Note that this time we used catch
method, which — because it gets called after the first then
— will catch any errors that get thrown in the chain above it. So whether the somePreviousPromise
or the callback in then
will fail — our Promise will handle it as intended in both of those cases.
As you can see, there are many subtleties when wrapping code in Promise constructor. That’s why it’s better to just use then
method to create new Promises, as we have shown in a second snippet. Not only it will look nicer, but we will also avoid those corner cases.
Consecutive thens vs parallel thens
Because many programmers have Object Oriented Programming backgrounds, it’s natural for them that a method mutates an object rather than creates a new one.
It’s probably why I see people being confused about what exactly happens when you call a then
method on a Promise.
Compare those two code snippets:
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult)
.then(doSecondThingWithResult);
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult);
somePromise
.then(doSecondThingWithResult);
Do they do the same thing? It might seem so. After all, both code snippets involve calling then
twice on somePromise
, right?
No. It’s a very common misconception. In fact, those two code snippets have a completely different behavior. Not fully understanding what is happening in both of them can lead to tricky mistakes.
As we wrote in a previous section, then
method creates a completely new, independent Promise. This means that in the first snippet, second then
method is not being called on somePromise
, but on a new Promise object, that encapsulates (or represents) waiting for somePromise
to get resolved and then calling doFirstThingWithResult
right after. And then we add a doSecondThingWithResult
callback to this new Promise instance.
In effect, the two callbacks will be executed one after another — we have a guarantee that the second callback will be called only after the first callback finishes execution without any issues. What is more, the first callback will get as an argument a value returned by somePromise
, but the second callback will get as an argument whatever is returned from the doFirstThingWithResult
function.
On the other hand, in the second code snipped, we call then
method on somePromise
twice and basically ignore two new Promises that get returned from that method. Because then
was called twice on exactly the same instance of a Promise, we don’t get any guarantees about which callback will get executed first. The order of execution here is undefined.
I sometimes think about it as “parallel” execution, in a sense that the two callbacks should be independent and not rely on any of them being called earlier. But of course in reality JS engines execute only one function at a time — you simply don’t know in which order they will be called.
The second difference is that both doFirstThingWithResult
and doSecondThingWithResult
in the second snippet will receive the same argument — the value that somePromise
gets resolved to. Values returned by both the callbacks are completely ignored in that example.
Executing a Promise immediately after creation
This misconception also comes from the fact that most coders are often experienced in Object Oriented Programming.
In that paradigm, it is often considered a good practice to make sure that an object constructor does not perform any actions by itself. For example an object representing a Database should not initiate the connection with the database when its constructor is called with the new
keyword.
Instead, it’s better to provide special method — for example called init
— that will explicitly create a connection. This way an object does not perform any unintended actions only because it was initiated. It patiently waits for a programmer to explicitly ask for executing an action.
But that’s not how Promises work.
Consider the example:
const somePromise = new Promise(resolve => {
// make HTTP request
resolve(result);
});
You might think that the function making an HTTP request does not get called here, because it is wrapped in a Promise constructor. In fact, many programmers expect that it gets called only after a then
method gets executed on a somePromise
.
But that’s not true. The callback gets executed immediately when that Promise is created. It means that when you are in the next line after creating somePromise
variable, your HTTP request is probably already being executed, or at least scheduled.
We say that a Promise is “eager” because it executes an action associated with it as fast as possible. In contrast, many people expect the Promises to be “lazy” — that is to perform an action only when it is absolutely necessary (for example when a then
gets called for the first time on a Promise). It’s a misconception. Promises are always eager and never lazy.
But what you should do if you want to execute the Promise later? What if you want hold off with making that HTTP request? Is there some magic mechanism built into the Promises that would allow you to do something like that?
The answer is more obvious than the developers sometimes would expect. Functions are a lazy mechanism. They are executed only when programmer explicitly calls them with a ()
bracket syntax. Simply defining a function doesn’t really do anything just yet. So the best way to make a Promise lazy is… to simply wrap it in a function!
Take a look:
const createSomePromise = () => new Promise(resolve => {
// make HTTP request
resolve(result);
});
Now we wrapped the same Promise constructor call in a function. Because of that nothing really gets called yet. We also changed a variable name from somePromise
to createSomePromise
, because it is not really a Promise anymore — it is a function creating and returning a Promise.
The Promise constructor — and hence the callback function with a HTTP request — will only be called when we execute that function. So now we have a lazy Promise, that gets executed only when we really want it.
What is more, note that for free we got another capability. We can easily create another Promise, that performs the same action.
If for some weird reason we would like to make the same HTTP call twice and execute those calls concurrently, we can just call the createSomePromise
function twice, one immediately after another. Or if a request fails for any reason, we can retry it, using the very same function.
This shows that it’s extremely handy to wrap Promises in functions (or methods) and hence it is a pattern that should become natural for a JavaScript developer.
Ironically, if you have read my article on Promises vs Observables, you know that programmers being introduced to Rx.js often make an opposite mistake. They code Observables as if they are eager (like Promises), while in fact they are lazy. So, for example, wrapping Observables in a function or a method often does not make any sense and in fact can even be harmful.
Conclusion
I have shown you three types of mistakes that I have often seen being made by developers who knew Promises in JavaScript only superficially.
Are there any interesting types of mistakes that you have encountered either in your code or in the code of others? If so, share them in the comment.
If you enjoyed this article, considered following me on Twitter, where I will be posting more articles on JavaScript programming.
Thanks for reading!
(Photo by Sebastian Herrmann on Unsplash)
Top comments (6)
Correct me if I am wrong but I think this piece of code:
Could be simplyfied by avoiding the external creation of the wrapping Promise:
Because returning the result will resolve the Promise chain, and the rejection will be forwarded.
Hey, you are absolutely correct. Wrapping promise into a promise is presented as an antipattern here. I will update an article to make that more clear!
Thanks!
Async functions also return promises that resolve with the return value.
So the last example can even be written like:
Small correction here ...
Thanks for making that clearer Kumar!
this is great information. these common misconceptions i have seen many times when reviewing code. some of them easily lead to bugs. thanks for writing it down precise and concise and explain it well. i have not seen / found that before, although i have googled for such an article