📜 ⬆️ ⬇️

We have problems with promises

Let me introduce you to the translation of Nolan Lawson's article “ We have problems with promises ”, one of the best topics I've ever read.

We have problems with promises


Dear JavaScript developers, the time has come to recognize this - we have problems with promises.

No, not with the promises themselves. Their implementation of the A + specification is excellent. The main problem, which itself appeared to me over the years of observing how many programmers are struggling with the rich API promises, is the following:
')
- Many of us use promises without a real understanding of them.

If you do not believe me, solve this problem:

Question: What is the difference between these four options for using promises?

doSomething().then(function () { return doSomethingElse(); }); doSomething().then(function () { doSomethingElse(); }); doSomething().then(doSomethingElse()); doSomething().then(doSomethingElse); 


If you know the answer, then let me congratulate you - on the part of promises you are a ninja. Perhaps you can not read further this post.

The rest of 99.99% of you, I am in a hurry to say that you are not upset, you are in good company. No one who responded to my tweet could solve the problem. Even I was very surprised by the answer to the 3rd question. Yes, despite the fact that I asked him!

The answer to the problem is given at the very end of the article, but first I would like to explain why promises in the first approximation are so insidious, and why many of us, beginners and experts, fall into their traps. I also want to offer you my solution, one unusual trick that will help you better understand the promises. And, of course, I believe that after my explanation they will not seem so complicated to you.

Before we begin, let's mark some points.

Why promises?


If you read articles about promises, you will often see references to the " pyramid of evil " ("pyramide of doom" in orig.), Formed by a terrible code on callbacks, which stretches the page to the right behind the screen.

Promises do solve this problem, but this is more than just reducing indents. As explained in the remarkable conversation “ Salvation from callbacks of hell, ” their real problem is that they make it impossible for us to use the return and throw instructions. Instead, the logic of our programs is based on the use of side effects when one function calls another.

Also callbacks do something really sinister, they deprive us of the stack, what is taken for granted in programming languages. Writing code without a stack is the same as driving a car without a brake pedal. You have no idea how important it is until you really do not need it and it is not there.

The whole point of the promises is to give us back the basics of the language that were lost at the time of our transition to asynchrony: return , throw, and stack. But you must know how to properly use promises to rise to a higher level in this.

Novice bugs


Someone is trying to explain the promises in the form of a cartoon or, saying: “Oh! This is a thing that you can create and transmit everywhere, and it symbolizes some value that is received and returned asynchronously .

I do not find such an explanation quite useful. For me, promises are always part of the code structure, its execution flow.

A small digression: the term "promises" for different people carries a different meaning. In this article I will talk about the official specification , available in modern browsers as window.Promise . For those browsers that do not have window.Promise , there is a nice polifil with a cheeky name Lie (false), containing a minimal implementation of the specification.

Error novice number 1 - "pyramid of evil" from promises


Looking at how people use PouchDB, whose API is heavily tied to promises, I see a lot of bad patterns for using them. Here is the most common example:

 remotedb.allDocs({ include_docs: true, attachments: true }).then(function (result) { var docs = result.rows; docs.forEach(function(element) { localdb.put(element.doc).then(function(response) { alert("Pulled doc with id " + element.doc._id + " and added to local db."); }).catch(function (err) { if (err.status == 409) { localdb.get(element.doc._id).then(function (resp) { localdb.remove(resp._id, resp._rev).then(function (resp) { //   … 

Yes, it turns out that we can use the promises as if they were callbacks, and yes, it is like shooting a gun at the sparrows.

If you think that only absolute beginners make such mistakes, I will surprise you - the example code above is taken from the official BlackBerry developers blog ! It is difficult to get rid of the old habits of using callbacks.

Here is a better option:

 remotedb.allDocs(...) .then(function (resultOfAllDocs) { return localdb.put(...); }) .then(function (resultOfPut) { return localdb.get(...); }) .then(function (resultOfGet) { return localdb.put(...); }) .catch(function (err) { console.log(err); }); 

In the example above, composite promises were used (“composing promises” in orig.) - one of the strongest sides of promises. Each subsequent function will be called when the previous promise is “killed”, and it will be called with the result of the previous promise. Details will be below.

Newbie error number 2 - how do I use forEach () with promises?


This is the moment when the understanding of promises by most people begins to pass. They are familiar with the forEach , for, or while iterators, but they have no idea how to combine them with promises. Then something like this is born:

 //    remove()    db.allDocs({include_docs: true}) .then(function (result) { result.rows.forEach(function (row) { //  remove  promise db.remove(row.doc); }); }) .then(function () { //     ,     ! }); 

What is wrong with this code? The problem is that the first function returns undefined , which means the second does not wait for the completion of db.remove () for all documents. In fact, it waits for nothing at all and will be executed when any number of documents are deleted, and maybe not even one.

This is a very insidious mistake, because at first you may not even notice it, especially if the documents are deleted quickly enough to update the interface. A bug can pop up only in rare cases, not in all browsers, which means it will be almost impossible to identify and fix it.

Summing up, I will say that constructions like forEach , for and while are “not the drones you are looking for . You need Promise.all () :

 db.allDocs({include_docs: true}) .then(function (result) { var arrayOfPromises = result.rows.map(function (row) { return db.remove(row.doc); }); return Promise.all(arrayOfPromises); }) .then(function (arrayOfResults) { //      ! }); 

What's going on here? Promise.all () accepts an array of promises as an argument and returns a new promise, which is “cleared” only when all documents are deleted. This is the asynchronous equivalent of a for loop.

Also promise from Promise.all () will transfer to the next function an array of results, which can be very convenient if you, for example, do not delete documents, but receive data from several sources at once. If at least one promise from the array passed to Promise.all () is "registered", then the resulting promise will go to the rejected state.

Newbie error number 3 - forget to add .catch ()


This is another common mistake - blissfully believing that your promises will never return an error. Many developers simply forget to add catch () anywhere in their code.

Unfortunately, this often means that errors will be “swallowed”. You never even know that they were - a special pain when debugging an application.

To avoid this unpleasant scenario, I made it a rule, which then grew into a habit of always adding a catch () method to the end of my promis chain:

 somePromise().then(function () { return anotherPromise(); }) .then(function () { return yetAnotherPromise(); }) //      : .catch(console.log.bind(console)); 

Even if you are guaranteed not to expect any errors at all, adding catch () is a reasonable solution. Then, if suddenly your assumption about mistakes is not justified, you will thank yourself.

Newbie error number 4 - use "deferred"


I see this error all the time, and I don’t even want to repeat the name of this object, fearing that he, like The Beatles from the film of the same name, is just waiting to increase the number of instances of his use.

In short, in its development, the promises have gone a long way. It took a lot of time for the javascript community to implement them correctly. Initially, jQuery and Angular used the pattern of deferred objects everywhere, which was later replaced by the specification of ES6 promises, which were based on the “good” libraries Q, When, RSVP, Bluebird, Lie and others.

In general, if you suddenly wrote this word in your code (I will not repeat it a third time!), Know that you are doing something wrong. Below is a recipe for how to avoid it.

Most "promis" libraries give you the ability to import promises from other libraries. For example, the $ q module from Angular allows you to wrap non- $ q promises with $ q.when () . That is, Angular users can wrap PouchDB promises like this:

 //  ,  : $q.when(db.put(doc)).then(/* ... */); 

Another way is to use the “revealing constructor pattern” pattern in the original. It is convenient for wrapping an API that does not use promises. For example, to wrap a callback-based Node.js API, you can do the following:

 new Promise(function (resolve, reject) { fs.readFile('myfile.txt', function (err, file) { if (err) { return reject(err); } resolve(file); }); }).then(/* ... */) 

Done! We dealt with a terrible defer ... Ah, I almost said the third time! :)

Novice Error 5 - using external functions instead of returning results


What is wrong with this code?

 somePromise().then(function () { someOtherPromise(); }) .then(function () { // ,   someOtherPromise «»… // , : ,  «». }); 

Well, now is the perfect moment to talk about everything that you should know about promises.

Seriously, this is the same trick, having understood that, you yourself will be able to avoid all those mistakes that we talked about. You are ready?

As I already mentioned, the magic of promises is that they return precious return and throw to us . But what does this mean in practice?

Each promise provides you with the then () method (and also catch () , which in practice is just “sugar” for then (null, ...) ). And here we are inside the then () function:

 somePromise().then(function () { // ,    then()! }); 

What can we do here? Three things:

  1. Return ( return ) another promise
  2. Return ( return ) synchronous value (or undefined )
  3. Throw a synchronous error

That's it, the whole trick. If you understand him, you will understand promises. Let's now analyze in detail each of the items.

1. Return another promise


This is a frequent pattern that you could see in all sorts of promise literature, as well as in the example with the composite promises above:

 getUserByName('nolan').then(function (user) { //  getUserAccountById  promise, //      then return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! }); 

Pay attention that I return the second promis, I use return . Using return here is a key point. If I just called getUserAccountById , then yes, there would be a request for user data, a result would be obtained that would not be useful anywhere - the next would then get undefined instead of the desired userAccount .

2. Return a synchronous value (or undefined)


Returning undefined as a result is a common mistake. But the return of any synchronous value is a great way to convert a synchronous code into a chain of promises. Suppose we have a cache of user data in our memory. We can:

 getUserByName('nolan').then(function (user) { if (inMemoryCache[user.id]) { //     , //   return inMemoryCache[user.id]; } //       , //    return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! }); 

Isn't it cool? The second function in the chain does not matter where the data came from, from the cache or as a result of the request, and the first is free to return either a synchronous value immediately or an asynchronous promise, which already in turn returns a synchronous value.

Unfortunately, if you did not use return , the function will still return a value, but it will no longer be the result of a call to the nested function, but a useless undefined , which is returned by default in such cases.

For myself, I made a rule, which then grew into a habit - always use return inside then or give an error with throw . I recommend that you do the same.

3. Issue a synchronous error


Here we come to throw . Here the promises start to shine even brighter. Suppose we want to issue ( throw ) a synchronous error if the user is not authorized. It's simple:

 getUserByName('nolan').then(function (user) { if (user.isLoggedOut()) { //   —  ! throw new Error('user logged out!'); } if (inMemoryCache[user.id]) { //     , //   return inMemoryCache[user.id]; } //       , //    return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! }) .catch(function (err) { // , ,     ! }); 

Our catch () will get a synchronous error if the user is not authorized, or asynchronous if any of the promises above go into the rejected state. And again, the functions in catch without a difference, were a synchronous or asynchronous error.

This is especially useful for tracing errors during development. For example, the formation of an object from a string using JSON.parse () somewhere inside then () may give an error if json is invalid. With callbacks, it will be “swallowed up,” but with the help of the catch () method we can easily handle it.

Advanced bugs


Well, now that you've learned the main trick of the promises, let's talk about extreme cases. Because there are always extreme cases.

I call this category of errors “advanced”, because I only met them in the code that are familiar with programmer promises. We need to discuss such errors in order to make out the puzzle that I published at the very beginning of the article.

Advanced error number 1 - do not know about Promise.resolve ()


I have already shown above how convenient promises are when wrapping synchronous logic in asynchronous code. You might have noticed something similar:

 new Promise(function (resolve, reject) { resolve(someSynchronousValue); }).then(/* ... */); 

Keep in mind, you can write the same thing much shorter:

 Promise.resolve(someSynchronousValue).then(/* ... */); 

Also, this approach is very convenient for catching any synchronous errors. It is so convenient that I use it in almost all API methods that return promises:

 function somePromiseAPI() { return Promise.resolve() .then(function () { doSomethingThatMayThrow(); return 'foo'; }) .then(/* ... */); } 

Just remember, any code that can produce a synchronous error is a potential debugging problem due to swallowed errors. But if you wrap it in Promise.resolve () , you can be sure that you will catch it with catch () .

There is also Promise.reject () . It can be used to return promise in rejected status:

 Promise.reject(new Error('-  ')); 

Advanced error # 2 - catch () is not the same with then (null, ...)


Just above, I mentioned that catch () is just “sugar”. The two examples below are equivalent:

 somePromise().catch(function (err) { //   }); somePromise().then(null, function (err) { //   }); 

However, the examples below are no longer “equal”:

 somePromise().then(function () { return someOtherPromise(); }) .catch(function (err) { //   }); somePromise().then(function () { return someOtherPromise(); }, function (err) { //   }); 

If you think about why the examples above are “not equal,” look carefully at what happens if an error occurs in the first function:

 somePromise().then(function () { throw new Error('oh noes'); }) .catch(function (err) { //  ! :) }); somePromise().then(function () { throw new Error('oh noes'); }, function (err) { // ?  ? O_o }); 

It turns out that if you use the then (resolveHandler, rejectHandler) format , then the rejectHandler in fact cannot catch the error that occurred inside the resolveHandler function.

Knowing this feature, for myself I introduced the rule never to use the second function in the then () method, and in return always add error handling below in the form of catch () . I have only one exception - asynchronous tests in Mocha , in cases when I deliberately wait for an error:

 it('should throw an error', function () { return doSomethingThatThrows().then(function () { throw new Error('I expected an error!'); }, function (err) { should.exist(err); }); }); 

By the way, Mocha and Chai are a great combination for testing promise-based APIs.

Advanced error number 3 - promises against promise factories


Suppose you want to perform a series of promises one after another, sequentially. You want something like Promise.all () , but one that does not perform promises in parallel.

In the heat of the moment, you can write something like this:

 function executeSequentially(promises) { var result = Promise.resolve(); promises.forEach(function (promise) { result = result.then(promise); }); return result; } 

Unfortunately, the example above will not work as intended. Promises from the list passed to executeSequentially () will still run in parallel.

The reason is that, according to the specification, the promis starts to execute the logic embedded in it immediately after its creation. He will not wait. Thus, not the promises themselves, but an array of promise factories - this is what really needs to be passed to executeSequentially :

 function executeSequentially(promiseFactories) { var result = Promise.resolve(); promiseFactories.forEach(function (promiseFactory) { result = result.then(promiseFactory); }); return result; } 

I know you are thinking now: “Who the hell is this Java programmer, and why does he tell us about factories?” . In fact, a factory is a simple function that returns a promise:

 function myPromiseFactory() { return somethingThatCreatesAPromise(); } 

Why this example will work? And because our factory will not create a promise until it is a turn. It works exactly as resolveHandler for then () .

Look closely at the executeSequentially () function and mentally replace the link to promiseFactory with its contents - now a light bulb should joyfully flash above your head :)

Advanced error # 4 - what if I want the result of two promises?


It often happens that one promise depends on the other, and we need the results of both at the output. For example:

 getUserByName('nolan').then(function (user) { return getUserAccountById(user.id); }) .then(function (userAccount) { // ,     «user» ! }); 

Wanting to remain good JavaScript developers, we may want to bring the user variable to a higher level of visibility so as not to create an “evil pyramid”.

 var user; getUserByName('nolan').then(function (result) { user = result; return getUserAccountById(user.id); }) .then(function (userAccount) { // ,    «user»,  «userAccount» }); 

It works, but personally I think that the code “smacks”. My decision is to push aside prejudice and take a conscious step towards the “pyramid”:

 getUserByName('nolan').then(function (user) { return getUserAccountById(user.id) .then(function (userAccount) { // ,    «user»,  «userAccount» }); }); 

... at least a temporary step. If you feel that the indentation is increasing and the pyramid begins to grow threateningly, do what JavaScript developers have done for centuries - create a function and use it by name.

 function onGetUserAndUserAccount(user, userAccount) { return doSomething(user, userAccount); } function onGetUser(user) { return getUserAccountById(user.id) .then(function (userAccount) { return onGetUserAndUserAccount(user, userAccount); }); } getUserByName('nolan') .then(onGetUser) .then(function () { //     doSomething() , //     —     }); 

As the code becomes more complex, you will notice that more and more of it is transformed into named functions, and the application logic itself begins to take on an aesthetic pleasure:

 putYourRightFootIn() .then(putYourRightFootOut) .then(putYourRightFootIn) .then(shakeItAllAbout); 

.

№5 — «»


, , . . , . , .

, ?

 Promise.resolve('foo') .then(Promise.resolve('bar')) .then(function (result) { console.log(result); }); 

, bar, . foo!

, , then() - (, ), then(null) «» . :

 Promise.resolve('foo') .then(null) .then(function (result) { console.log(result); }); 

then(null) , — foo.

. . , then() , , . then() . , - :

 Promise.resolve('foo') .then(function () { return Promise.resolve('bar'); }) .then(function (result) { console.log(result); }); 

bar, .

: then() .


, , , , , .

.

№1


 doSomething().then(function () { return doSomethingElse(); }) .then(finalHandler); 

Answer:

 doSomething |-----------------| doSomethingElse(undefined) |------------------| finalHandler(resultOfDoSomethingElse) |------------------| 

№2


 doSomething().then(function () { doSomethingElse(); }) .then(finalHandler); 

Answer:

 doSomething |-----------------| doSomethingElse(undefined) |------------------| finalHandler(undefined) |------------------| 

№3


 doSomething().then(doSomethingElse()) .then(finalHandler); 

Answer:

 doSomething |-----------------| doSomethingElse(undefined) |---------------------------------| finalHandler(resultOfDoSomething) |------------------| 

№4


 doSomething().then(doSomethingElse) .then(finalHandler); 

Answer:

 doSomething |-----------------| doSomethingElse(resultOfDoSomething) |------------------| finalHandler(resultOfDoSomethingElse) |------------------| 

, , doSomething() doSomethingElse() , . , - .

.


. , . , , , .

, - — PounchDB map/reduce . : 290 , 555 . -, … . .

, . , . , , . , - , . , , . . , , , , .

async/await


« ES7 » async/await , . - ( catch() , try/catch , , ES7 try/catch/return .

JavaScript, , - - , .

JavaScript, , JSLint JSHint , « JavaScript », . , , , , .

async/await , JS, - - . , , ES5 ES6.

« JavaScript», . , :

— !

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


All Articles