Jun 01, 2021 Article blog
1. Wrap everything in the Promise constructor
2. A comparison of a serial call to a parallel call of then
The article was reproduced from the public number: Imprint Chinese
Original link: dev.to/mpodlasin/3-most-common-mistakes-when-using-promises-in-javascript-oab
Translator: Shopee Financial Front End Team Zhang Tieshan
This article summarizes several errors that developers often make when writing
Promise
and analyzes them to see if they're written by you?
Today, even with the introduction of async/await, the writing rules of
Promises
in
JavaScript
are still essential knowledge for all JS developers.
JavaScript deals with asynchronous issues differently from other programming languages. A s a result, even experienced developers sometimes get caught up in mistakes. I've seen first-hand that good Python or Java programmers make very stupid mistakes when encoding node .js or browsers.
To avoid these
Promises
in
JavaScript
has a lot of writing details to consider. S
ome of them are purely language style issues, but there are also many errors that are actually introduced and difficult to track.
So I decided to write a checklist of the three most common
Promises
encounter when programming with Promises.
The first mistake was also one of the most obvious, but I found that developers made it surprisingly often.
When you first learn about Promises, you'll learn about Promise's constructor, which can be used to create a new Promises object.
Perhaps because people often start learning by wrapping some browser APIs, such as
setTimeout
in a Promise constructor, there is a deep-seated belief in their hearts that the only way to create a Promise object is to use a constructor.
Therefore, it is usually written as:
const createdPromise = new Promise(resolve => {
somePreviousPromise.then(result => {
// 对 result 进行一些操作
resolve(result);
});
});
As you can see, some people use
then
in order to do something about the result
result
somePreviousPromise
but then decide to wrap it again in a Promise constructor in order to store the result of the operation in the createdPromise variable, presumably to do more on the Promise later.
This is clearly unnecessary.
The full point of the
then
method is that it itself returns a Promise, which represents a callback function in
then
after the execution of
somePreviousPromise
then
the argument of which is the result of the successful execution of the return by
somePreviousPromise
Therefore, the previous piece of code is roughly equivalent to:
const createPromise = somePreviousPromise.then(result => {
// 对 result 进行一些操作
return result
})
Writing like this would be a lot easier.
But why do I say it's just roughly equivalent? What's the difference?
Inexperienced and inexperienced observations can be difficult to detect, but there is actually a huge difference in error handling that is more important than the redundancy of the first piece of code.
Suppose
somePreviousPromise
fails for some reason and throws an error.
For example, an HTTP request was sent in this Promise, and the API responded to a 500 error.
As it turns out, in the previous piece of code, we wrapped one Promise into another, and we couldn't catch the error at all. To resolve this issue, we must make the following changes:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// 对 result 进行一些操作
resolve(result);
}, reject);
});
We simply added a
reject
argument to the callback function and then used it by passing it to
then
as a second argument.
It is important to remember that
then
method accepts a second optional parameter for error handling.
Now if
somePreviousPromise
fails for some reason,
reject
function will be called and we will be able to handle errors on
createdPromise
as usual.
Does this solve all the problems? I'm sorry, but I didn't.
We handled the errors that might have occurred
somePreviousPromise
itself, but we still can't control what
then
in the callback function as the first argument of the then method.
There may be some errors in the code performed in the comment area
// 对 result 进行一些操作
and if the code in this area throws any errors, the second parameter of the
then
reject
still does not catch them.
This is because the error handler, which is the second argument to the
then
method, responds only to errors that occurred before the current
then
on the Promise chain.
Therefore, the most appropriate (and final) solution should be as follows:
const createdPromise = new Promise((resolve, reject) => {
somePreviousPromise.then(result => {
// 对 result 进行一些操作
resolve(result);
}).catch(reject);
});
Note that this time we used
catch
method -- because it will be called after the first
then
it will catch all the errors thrown on the Promise chain.
Whether the callback in
somePreviousPromise
or
then
fails,
Promise
will handle these situations as expected.
As can be seen from the example above, there are many details to be addressed when wrapping code in Promise's constructor. T
hat's why it's best to create a new Promises using the
then
method, as shown in the second piece of code.
Not only does it look elegant, but it also helps us avoid extreme situations.
Because many programmers have an object-oriented programming background, it is common for them to call a method to change an object rather than create a new object.
That's probably why I see people confused about what happened
"when I
then
method on Promise."
Compare the following two pieces of code:
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult)
.then(doSecondThingWithResult);
const somePromise = createSomePromise();
somePromise
.then(doFirstThingWithResult);
somePromise
.then(doSecondThingWithResult);
Do they do the same thing?
It looks the same, after all, both pieces of code were raised twice in
somePromise
right?
then
No, this is another very common misconception. I n fact, the two pieces of code do things completely differently. Failure to fully understand what is being done in the two pieces of code can lead to very tricky errors.
As we said in the previous sections,
then
method creates a completely new, independent Promise. T
his means that in the first piece of code, the second
then
method is not called on
somePromise
but on a new Promise object, which means that
doFirstThingWithResult
is called as soon as the state of
somePromise
successful.
Then add a callback action
doSecondThingWithResult
to the newly returned Promise instance
In fact, the two callbacks will be executed one by one -- ensuring that the second callback is called only after the first callback is complete and there are no problems.
In addition, the first callback will receive the value returned by
somePromise
as an argument, but the second callback function will receive the value returned by the
doFirstThingWithResult
function as an argument.
On the other hand, in the second piece of code, we called the
then
method twice on
somePromise
essentially ignoring the two new Promises objects returned from that method.
Because
then
was called twice on the exact same Promise instance, we can't determine which callback to execute first, and the order of execution here is uncertain.
In a sense, the two callbacks should be independent and not dependent on any previous callbacks, which I sometimes consider to be "parallel" execution. But, of course, in fact, the JS engine can only perform one function at a time -- you simply don't know in what order they will be called.
The second difference between the two pieces of code is that in the second piece of code,
doFirstThingWithResult
and
doSecondThingWithResult
receive the same argument --
somePromise
successfully executes the returned result, and the return values of the two callback functions are completely ignored in this example.
This misunderstanding also arises because most programmers have extensive experience in object-oriented programming.
In the idea of object-oriented programming, it is often considered a good practice to ensure that the object's constructor itself does nothing.
For example, an object that represents a database should not initiate a link to the database when calling its constructor using the
new
keyword.
Instead, you should provide a specific method, such as calling a method called
init
it explicitly creates a connection. T
his way, an object does not perform any unexpected actions because it has been created.
It is executed as clearly required by the programmer.
But this is "not how Promises work."
Consider the following example:
const somePromise = new Promise(resolve => {
// 创建 HTTP 请求
resolve(result);
});
You might think that the function that made the HTTP request was not called here because it was wrapped in the Promise constructor.
In fact, many programmers want the
then
method to be executed on
somePromise
before it is called.
But that is not the case. O
nce the Promise is created, the callback is executed immediately.
This means that when you go to the next line after you create the
somePromise
variable, your HTTP request may have been executed, or an execution queue already exists.
We say Promise is "eager" because it performs the actions associated with it as quickly as possible. I
nstead, many expect Promises to be "lazy", that is, called only when necessary (for example, when
then
method is first called on Promise).
This is a myth that Promise is always eager, not lazy.
But what should you do if you want to delay performing Promise? W hat if you want to delay making this HTTP request? Is there a fancy mechanism built into Promises that lets you do something similar?
The answer sometimes exceeds developers' expectations. A
function is a lazy mechanism. P
rogrammers execute them only if they are explicitly called using the
()
syntax. D
efining just one function doesn't actually do anything.
So the best way to make Promise "lazy" is to wrap it simply in a function!
The code is as follows:
const createSomePromise = () => new Promise(resolve => {
// 创建 HTTP 请求
resolve(result);
});
Now we wrap the call operation of the Promise constructor in one function. I
n fact, it hasn't really been called yet.
We also changed the variable name from
somePromise
to
createSomePromise
because it is no longer a Promise object -- but a function that creates and returns the Promise object.
The Promise constructor (and the callback function with an HTTP request) is called only when the function is executed. So now we have a lazy Promise that executes only when we really want it to execute.
Also, note that it comes with another feature. We can easily create another Promise object that can do the same.
If, for some strange reason, we want to make two identical HTTP requests and execute them simultaneously, we only need to call
createSomePromise
function twice.
Or, if the request fails for any reason, we can re-request it using the same function.
This indicates that wrapping Promises in a function (or method) is convenient, so it should be natural for JavaScript developers to develop using this pattern.
Ironically, if you read my article Promises vs Observables, you'll know that programmers writing Rx .js often make the opposite mistake. T hey encode Observable as if they were "eager" (consistent with Promises), when in fact they are "lazy". Therefore, encapsulating Observables in a function or method often makes no sense, and is actually even harmful.
This article shows three types of errors that developers often see when using Promise, because their understanding of Promises in JavaScript is only superficial.
Here's a description of the three
most common errors in using Promises in JavaScript
from
W3Cschool编程狮