📜 ⬆️ ⬇️

Promise guide for those who want to understand them

The forest is wonderful, dark - look at the depths.
But first, I will return all debts ...
And many miles, until I fall asleep,
So many miles, until I fall asleep ...
Robert Frost

image

Promises are one of the most exciting innovations of ES6. JavaScript supports asynchronous programming via callback functions and other mechanisms . However, when using callback functions, we run into some problems. Among them - " hell kollbekov " and " pyramid of horror ." Promises are a pattern that greatly simplifies asynchronous programming on JS. Asynchronous code written using promises looks like synchronous and devoid of problems associated with callbacks.
')
The material, the translation of which we are publishing today, is dedicated to promises and their practical use. It is designed for novice developers who want to deal with promises.

What is promis?


Here is the definition of promises given by ECMA: “Promis is an object that is used as a placeholder for a possible future outcome of deferred (and possibly asynchronous) calculations.

Simply put - promise is a container for some future value. It is worth noting that often speaking of promises, they are called “promises” and “promised results.” If you think a little, it’s like people use the word “promise” in everyday life. For example, you booked a plane ticket that flies to India. There you are going to visit the beautiful mountain station Darjeeling . After completing the booking process, you receive a ticket. This ticket, in fact, is the promise of the airline to give you a seat on the plane the day you want to go. In general, a ticket is a placeholder for future values, namely, for a seat in an airplane.

Here is another example. You promised a friend that you would return his book The Art of Programming to him after you read it. In this case, the placeholder is your words. And the “future result” is a book.

In ordinary life, you can find other similar situations. For example, waiting at the receptionist, ordering food in a restaurant, issuing a book in the library, and many others. All of these situations include some form of promise. Perhaps we gave fairly simple examples, so let's deal with the code .

Creating promises


Promises are created in situations where it is not possible to say exactly how long it takes to perform an operation, or it is expected that this operation will take a very long time. For example, a network request may take from 10 to 200 ms, depending on the connection speed. We do not want to wait for the receipt of this data. For a person, 200 ms is very little, but for a computer it is a very significant length of time. Promises simplify and facilitate the solution of such problems.

A new promise can be created by resorting to the Promise designer. It looks like this.

 const myPromise = new Promise((resolve, reject) => {   if (Math.random() * 100 <= 90) {       resolve('Hello, Promises!');   }   reject(new Error('In 10% of the cases, I fail. Miserably.')); }); 

Notice that the constructor takes a function with two parameters. This function is called the executor function, it describes the calculations that must be performed. Parameters are usually called resolve and reject , they are, respectively, used to indicate the successful and unsuccessful completion of the performing function.

The resolve and reject parameters are also functions, they are used to return values ​​to the promise object. If the calculation succeeds, or the future value is ready, we send this value using the resolve function. In such a situation they talk about the successful resolution of promis.

If the calculations could not be performed or an error occurred during the work, we report this by passing the error object to the reject function. In this case, they say that promis is rejected. Actually, the reject function accepts any value, however, it is recommended to pass an Error object to it, as this helps during debugging during stack tracing.

In our example, the Math.random() function is used to generate random numbers. In 90% of cases, based on the equal probability of issuing different random numbers, the promise will be resolved. In other cases, it will be rejected.

Use of promises


Above, we created a promise and saved a link to it in myPromise . How to get access to the values ​​passed by the functions resolve and reject ? The .then() function, which is available to all promises, will help us with this. Take a look at how to work with her.

 const myPromise = new Promise((resolve, reject) => {   if (Math.random() * 100 < 90) {       console.log('resolving the promise ...');       resolve('Hello, Promises!');   }   reject(new Error('In 10% of the cases, I fail. Miserably.')); }); //   const onResolved = (resolvedValue) => console.log(resolvedValue); const onRejected = (error) => console.log(error); myPromise.then(onResolved, onRejected); //   ,      myPromise.then((resolvedValue) => {   console.log(resolvedValue); }, (error) => {   console.log(error); }); //  ( 90% ) // resolving the promise ... // Hello, Promises! // Hello, Promises! 

The .then() method accepts two callback functions. The first is called when promise is enabled. The second is performed if the promis is rejected.

Notice the two functions, onResolved and onRejected . In the role of callback functions, they are passed to the .then() method. You can write the same shorter, it is shown in the same example below. The capabilities of such a design do not differ from those of the one where the functions were described before transferring them .then() .

Here I would like to pay special attention to several important things. We created a myPromise myPromise . Then we doubled the .then() handler to it. Both the one and the other have the same functionality, but they are perceived as different entities. In this regard, the following should be noted:


All this means that as soon as the promise reaches its final state, this state does not change (that is, the calculations are not performed again) even if you connect several .then() handlers to the promise.

To check this, you can take a look at the console.log() call at the very beginning of the example. When the code is run and two .then() handlers are attached to it, the console.log() call will be executed only once. This indicates that promis caches the result and, when another .then() connected, does the same.

Another important thing to pay attention to is that promises use a strategy of energetic calculations . With this approach, calculations in promis begin immediately after it is declared and the reference to it is written to a variable. There are no methods like .start() or .begin() that could be used to force the promise to run. In the previous example, this is exactly what happens.

In order to make the processing of promises used a strategy of lazy calculations, so that they are not called instantly, they are wrapped in a function. We will talk about this later.

Error handling in promises


Until now, in order not to complicate the narration, we have considered only cases of successful resolution of the promises. Let's talk now about what happens when an error occurs in the execution function. In this situation, the second callback .then() is called, that is, the onRejected function. Consider an example.

 const myPromise = new Promise((resolve, reject) => { if (Math.random() * 100 < 90) {   reject(new Error('The promise was rejected by using reject function.')); } throw new Error('The promise was rejected by throwing an error'); }); myPromise.then( () => console.log('resolved'), (error) => console.log(error.message) ); //  ( 90% ) // The promise was rejected by using reject function. 

The example is exactly the same as the previous one, with the difference that the promise will now be rejected with a probability of 90% and will give an error in the remaining 10% of cases.

Here the onResolved and onRejected callback functions are declared. Please note that the onRejected onRejected will be executed even if an error is thrown during the execution of the promise code. There is no need to explicitly reject the promise by passing the object of the error to the reject function. That is, the promise will be rejected in both cases.

Since error handling is a prerequisite for developing reliable programs, there is a special mechanism for working with errors in promises. Instead of writing something like .then(null, () => {...}) , if we need to handle errors, we can use the .catch(onRejected) construct, which accepts one callback - onRejected . This is how the new fragment of the above code will look when adding this mechanism to it.

 myPromise.catch( (error) => console.log(error.message) ); 

Remember that .catch() , in fact, just “ syntactic sugar ” for .then(undefined, onRejected) .

Combination of promises in chains


The methods .then() and .catch() always return promises. Therefore, you can combine multiple .then() calls into chains. Let's sort it out by example.

First, create a delay() function that returns a promise. Returned promis will be resolved after a specified time. This is what this function looks like.

 const delay = (ms) => new Promise( (resolve) => setTimeout(resolve, ms) ); 

In this example, we use the function to wrap a promise in it, with the result that the promise will not be executed immediately. The delay() function takes, as a parameter, the time expressed in milliseconds. The executing function has access to the ms parameter due to the closure . Here, in addition, there is a call to setTimeout() , which calls the resolved function after the specified number of milliseconds has passed, which leads to resolution of promise. Here is how to use this feature.

 delay(5000).then(() => console.log('Resolved after 5 seconds')); 

And here is how to combine several .then() calls into a chain.

 const delay = (ms) => new Promise( (resolve) => setTimeout(resolve, ms) ); delay(2000) .then(() => {   console.log('Resolved after 2 seconds')   return delay(1500); }) .then(() => {   console.log('Resolved after 1.5 seconds');   return delay(3000); }).then(() => {   console.log('Resolved after 3 seconds');   throw new Error(); }).catch(() => {   console.log('Caught an error.'); }).then(() => {   console.log('Done.'); }); // Resolved after 2 seconds // Resolved after 1.5 seconds // Resolved after 3 seconds // Caught an error. // Done. 

This code starts with the line where the delay function is called. Then the following happens here:


Also, pay attention to the code snippet where we execute the throw new Error() command, that is, we throw an error into .then() . This means that the current promise will be rejected, and the next .catch() handler will be called. As a result, the line Caught an error is displayed in the log. That is why the .then() block that .catch() after .catch() called.

Recommended, for error handling, to use. catch() , not .then() with onResolved and onRejected . Here is the code that clarifies this recommendation.

 const promiseThatResolves = () => new Promise((resolve, reject) => { resolve(); }); //   UnhandledPromiseRejection promiseThatResolves().then( () => { throw new Error }, (err) => console.log(err), ); //    promiseThatResolves() .then(() => {   throw new Error(); }) .catch(err => console.log(err)); 

At the very beginning of this example, we create a promise that is always allowed. When there is a .then() with two callbacks, onResolved and onRejected , you can handle errors and situations in which the promise is rejected, only for the performing function. Suppose the handler in .then() also throws an error. This, as can be seen from the code, will not result in a call to the onRejected onRejected .

However, if there is a .catch( ) block after .then() , this block will intercept both .catch( function errors and .then() errors. This makes sense, since .then() always returns a promise.

Results


You can independently perform all the examples, which will allow you, through practice, to better master what was discussed in this material. In order to study promises, you can practice in the implementation of functions based on callbacks in the form of promises. If you are working in Node.js, note that many functions in fs and in other modules are based on callbacks. There are utilities that allow you to automatically convert such functions into constructions based on promises. Let's say it's util.promisify from Node.js, and pify .

However, if you are only learning all this, it is recommended to adhere to the principle of WET (Write Everything Twice, write everything two times) and implement independently (or, at least, carefully read) as much code as possible of the libraries under study. In other cases, especially if you are writing code that goes into production, stick to the DRY principle (Don't Repeat Yourself, Don't Repeat). In terms of working with promises, there is a lot more that does not fall into this material. For example, these are static methods Promise.all , Promise.race , and others. In addition, error handling is very briefly covered here. There are well-known anti-patterns and subtleties that you should know about when working with promises. Here are some materials that would be useful for those who are interested in all this: ECMA specification , Mozilla Docs materials , Google promises guide, Exploring JS chapter on promises, useful article on promises basics.

Dear readers! How do you write asynchronous code in javascript?

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


All Articles