📜 ⬆️ ⬇️

Promises in ES6: Patterns and Anti-Patterns

A few years ago, when I started working at Node.js, I was horrified by what is now known as “ hell callbacks ”. But then from this hell to get out was not so easy. However, these days Node.js includes the latest, most interesting JavaScript features. In particular, Node, starting with version 4 , supports promises. They allow you to get away from complex structures consisting of callbacks.

image

Using promises instead of callbacks leads to writing more concise code that is easier to read. However, to someone who is not familiar with them, they may not seem particularly clear. In this article I want to show the basic templates of working with promises and share a story about the problems that their inept use can cause.
')
Please note that here I will use the arrow functions . If you are not familiar with them, it is worth saying that they are made easy, but in this case I advise you to read the material about their features.

Patterns


In this section, I will talk about promises, and how to use them correctly, demonstrating several patterns of their use.

▍Use of promises


If you use a third-party library that already supports promises, it’s pretty easy to use them. Namely, you need to pay attention to two functions: then() and catch() . For example, we have an API with three methods: getItem() , updateItem() , and deleteItem() , each of which returns a promise:

 Promise.resolve() .then(_ => {   return api.getItem(1) }) .then(item => {   item.amount++   return api.updateItem(1, item); }) .then(update => {   return api.deleteItem(1); }) .catch(e => {   console.log('error while working on item 1'); }) 

Each then() call creates another step in the chain of promises. If an error occurs anywhere in the chain, the catch() block is called, which is located behind the failed section. The then() and catch() methods can either return a value or a new promise, and the result will be passed to the next then() operator in the chain.

Here, for comparison, the implementation of the same logic using callbacks:

 api.getItem(1, (err, data) => { if (err) throw err; item.amount++; api.updateItem(1, item, (err, update) => {   if (err) throw err;   api.deleteItem(1, (err) => {     if (err) throw err;   }) }) }) 

The first difference of this code fragment from the previous one is that in the case of callbacks, we must include error handling at each step of the process, instead of using a single block to handle all errors. The second problem with callbacks is more related to style. A block of code representing each of the steps is horizontally aligned, which makes it difficult to perceive the sequence of operations that is obvious when looking at the code based on promises.

▍ Conversion of callbacks into promises


One of the first techniques that is useful to study when moving from callbacks to promises is to convert callbacks to promises. The need for this may arise if, for example, you work with a library that still uses callbacks, or with your own code written with their use. Going from callbacks to promises is not so difficult. Here is an example of converting a Node fs.readFile function based on callbacks into a function that uses promises:

 function readFilePromise(filename) { return new Promise((resolve, reject) => {   fs.readFile(filename, 'utf8', (err, data) => {     if (err) reject(err);     else resolve(data);   }) }) } readFilePromise('index.html') .then(data => console.log(data)) .catch(e => console.log(e)) 

The cornerstone of this feature is the Promise designer. It takes a function, which, in turn, has two parameters - resolve and reject , which are also functions. Inside this function, all the work is done, and when we complete it, we call resolve if successful, and reject if an error has occurred.

Note that the result must be caused by one thing - either resolve or reject , and this call must be executed only once. In our example, if fs.readFile returns an error, we pass this error to reject . Otherwise, we pass the file data to resolve .

▍ Conversion of values ​​into promises


In ES6, there are a couple of convenient helper functions for creating promises from ordinary values. These are Promise.resolve() and Promise.reject() . For example, you may have a function that needs to return a promise, but which handles some cases synchronously:

 function readFilePromise(filename) { if (!filename) {   return Promise.reject(new Error("Filename not specified")); } if (filename === 'index.html') {   return Promise.resolve('<h1>Hello!</h1>'); } return new Promise((resolve, reject) => {/*...*/}) } 

Note that you can pass anything (or nothing) when calling Promise.reject() , however, it is recommended that you always pass an Error object to this method.

▍ Simultaneous execution of promises


Promise.all() — a convenient method for simultaneously executing an array of promises. For example, let's say we have a list of files that we want to read from the disk. Using the previously created readFilePromise function, the solution to this problem may look like this:

 let filenames = ['index.html', 'blog.html', 'terms.html']; Promise.all(filenames.map(readFilePromise)) .then(files => {   console.log('index:', files[0]);   console.log('blog:', files[1]);   console.log('terms:', files[2]); }) 

I will not even try to write equivalent code using traditional callbacks. Suffice to say that such a code will be confusing and error prone.

▍Secondary promises


Sometimes the simultaneous execution of several promises can lead to trouble. For example, if you try to get a lot of resources from the API using Promise.all , this is an API, after a while, when you exceed the limit on the frequency of calls to it, it may well start to generate error 429 .

One solution to this problem is to run the promises sequentially, one after the other. Unfortunately, in ES6 there is no simple analogue of Promise.al l to perform such an operation (I would like to know why?), But the Array.reduce method can help us here:

 let itemIDs = [1, 2, 3, 4, 5]; itemIDs.reduce((promise, itemID) => { return promise.then(_ => api.deleteItem(itemID)); }, Promise.resolve()); 

In this case, we want to wait for the current call to api.deleteItem() before making the next call. This code demonstrates a convenient way of processing the operation, which would otherwise have to be rewritten using then() for each element identifier:

 Promise.resolve() .then(_ => api.deleteItem(1)) .then(_ => api.deleteItem(2)) .then(_ => api.deleteItem(3)) .then(_ => api.deleteItem(4)) .then(_ => api.deleteItem(5)); 

▍ Promise Racing


Another handy helper function that exists in ES6 (although I don’t use it very often) is Promise.race . As well as Promise.all , it accepts an array of promises and executes them simultaneously, however, it is returned from it as soon as any of the promises is completed or rejected. The results of other promises are discarded.

For example, let's create a promise that completes with an error after some time, setting a limit on the operation to read a file represented by another promise:

 function timeout(ms) { return new Promise((resolve, reject) => {   setTimeout(reject, ms); }) } Promise.race([readFilePromise('index.html'), timeout(1000)]) .then(data => console.log(data)) .catch(e => console.log("Timed out after 1 second")) 

Please note that other promises will continue to run - you just will not see their results.

▍ Interception of errors


The usual way to trap errors in promises is to add a .catch() block to the end of the chain, which will intercept errors that occur in any of the preceding .then() blocks:

 Promise.resolve() .then(_ => api.getItem(1)) .then(item => {   item.amount++;   return api.updateItem(1, item); }) .catch(e => {   console.log('failed to get or update item'); }) 

The catch() block is called here if either getItem or updateItem fails. But what if we don’t need joint error handling and need to handle errors occurring in getItem separately? To do this, just insert another catch() block immediately after the block with the getItem — call getItem — it can even return another promise:

 Promise.resolve() .then(_ => api.getItem(1)) .catch(e => api.createItem(1, {amount: 0})) .then(item => {   item.amount++;   return api.updateItem(1, item); }) .catch(e => {   console.log('failed to update item'); }) 

Now, if getItem() fails, we intervene and create a new item.

▍Flashing errors


The code inside the then() expression should be perceived as if it were inside a try block. Both the return Promise.reject() call return Promise.reject() and the throw new Error() call will execute the next catch() block.

This means that runtime errors also trigger catch() blocks, so when it comes to error handling, you should not make assumptions about their source. For example, in the following code snippet, we can expect the catch() block to be called only to handle errors that occurred during getItem , but, as the example shows, it also responds to runtime errors that occur inside the then() expression:

 api.getItem(1) .then(item => {   delete item.owner;   console.log(item.owner.name); }) .catch(e => {   console.log(e); // Cannot read property 'name' of undefined }) 

▍Dynamic Promise Chains


Sometimes you need to construct a chain of promises dynamically, that is, by adding additional steps when certain conditions are met. In the following example, before reading the specified file, we, if necessary, create a lock file:

 function readFileAndMaybeLock(filename, createLockFile) { let promise = Promise.resolve(); if (createLockFile) {   promise = promise.then(_ => writeFilePromise(filename + '.lock', '')) } return promise.then(_ => readFilePromise(filename)); } 

In this situation, you need to update the promise value, using the construction of the form promise = promise.then(/*...*/) . Associated with this example is what we will discuss below in the “Multiple .then () call” section.

Anti-patterns


Promises are a neat abstraction, but working with them is full of pitfalls. Here we look at some of the typical problems that I have encountered while working with promises.

▍ Reconstruction of hell callbacks


When I started to switch from callbacks to promises, I found that it was hard to give up some old habits, and I found myself investing promises in each other just like callbacks:

 api.getItem(1) .then(item => {   item.amount++;   api.updateItem(1, item)     .then(update => {       api.deleteItem(1)         .then(deletion => {           console.log('done!');         })     }) }) 

In practice, such structures are not required almost never. Sometimes one or two levels of nesting can help group related tasks, but nested promises can almost always be rewritten as a vertical chain consisting of .then() .

▍Lack of return command


A common and harmful mistake I encountered is that they forget about calling return in the promise chain. For example, can you find an error in this code?

 api.getItem(1) .then(item => {   item.amount++;   api.updateItem(1, item); }) .then(update => {   return api.deleteItem(1); }) .then(deletion => {   console.log('done!'); }) 

The error is that we did not place a call to return before api.updateItem on line 4, and this particular block then() resolved immediately. As a result, api.deleteItem() is likely to be called before the api.updateItem() call is completed.

In my opinion, this is a major problem with ES6 promises, and it often leads to their unpredictable behavior. The problem is that then() can return either a value or a new Promise object, but it can also return undefined . Personally, if I were in charge of the JavaScript pre-API API, I would have provided for a run-time error if the .then() block returned undefined . However, this is not implemented in the language, so now we only need to be careful and perform an explicit return from any promise we create.

▍ Multiple call .then ()


In accordance with the documentation, it is possible to call .then() many times in the same promise, and callbacks will be called in the same order in which they are registered. However, I have never seen a real reason for doing so. Such actions can lead to incomprehensible effects when using return values ​​of promises and when handling errors:

 let p = Promise.resolve('a'); p.then(_ => 'b'); p.then(result => { console.log(result) // 'a' }) let q = Promise.resolve('a'); q = q.then(_ => 'b'); q = q.then(result => { console.log(result) // 'b' }) 

In this example, since we do not update the value of p on the next call to then() , we will never see the return of 'b' . Promis q more predictable; we update it every time by calling then() .

The same applies to error handling:

 let p = Promise.resolve(); p.then(_ => {throw new Error("whoops!")}) p.then(_ => { console.log('hello!'); // 'hello!' }) let q = Promise.resolve(); q = q.then(_ => {throw new Error("whoops!")}) q = q.then(_ => { console.log('hello'); //      }) 

Here we expect an error to be issued that will interrupt the execution of the promise chain, but since the value of p not updated, we end up in the second then() .

The multiple .then() call allows you to create several new independent promises from the original promise, however, I still have not managed to find a real use for this effect.

▍ Mixing callbacks and promises


If you are using a library based on promises, but are working on a project based on callbacks, it’s easy to fall into another trap. Avoid calling callbacks from then() or catch() — blocks catch() — otherwise, the promis will absorb all of the following errors, processing them as part of the promise chain. Here is an example of wrapping a promise into a callback, which, at first glance, may seem quite suitable for practical use:

 function getThing(callback) { api.getItem(1)   .then(item => callback(null, item))   .catch(e => callback(e)); } getThing(function(err, thing) { if (err) throw err; console.log(thing); }) 

The problem here is that in case of an error, we will get a warning "Unhandled promise rejection", despite the fact that the catch() block is present in the chain. This is because the callback() is called both inside then() and inside catch() , which makes it part of the promise chain.

If you absolutely need to wrap a promise in a callback, you can use the setTimeout , or process.nextTick function in Node.js to exit the promise:

 function getThing(callback) { api.getItem(1)   .then(item => setTimeout(_ => callback(null, item)))   .catch(e => setTimeout(_ => callback(e))); } getThing(function(err, thing) { if (err) throw err; console.log(thing); }) 

▍Uncoordinated errors


Error handling in JavaScript is a weird thing. It supports the classic try/catch paradigm, but it does not support error handling in the called code by its calling construct, as it is done, for example, in Java. However, in JS it is common to use callbacks, the first parameter of which is the error object (this callback is also called “errback”). This forces the construct calling the method to at least consider the possibility of an error. Here is an example with the fs library:

 fs.readFile('index.html', 'utf8', (err, data) => { if (err) throw err; console.log(data); }) 

When working with promises, it is easy to forget that errors must be explicitly handled. This is especially true in those cases when it comes to operations that are susceptible to errors, such as commands for working with the file system or for accessing databases. In the current environment, if you do not intercept the rejected promis, in Node.js you can see a rather ugly warning:

 (node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops! (node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. 

To avoid this, do not forget to add catch() to the end of the promise chain.

Results


We looked at some patterns and anti-patterns of using promises. I hope you found something useful here. However, the topic of promises is quite extensive, so here are a few links to additional resources:


Dear readers! How do you use promises in your Node.js projects?

Source: https://habr.com/ru/post/339414/


All Articles