Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

The three most common errors when using Promises in JavaScript


Jun 01, 2021 Article blog


Table of contents


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.

Wrap everything in the Promise constructor

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.

A comparison of a serial call to a parallel call of then

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.

Make a promise as soon as it is created

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.

epilogue

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编程狮