Javascript users, it’s time to admit that promises have been a problem for us. Promises are not wrong, promises /A+ are wonderful.
One of the biggest issues I’ve seen over the years as users of the PouchDB API and other Promise-heavy apis wrestle with these apis is this:
Most people who use Promises don’t really understand them
If you don’t agree with that, take a look at this question I recently posed on Twitter:
Q: What are the differences between the four promises
If you know the right answer, congratulations. You are a Big Promise, and you have no choice but to stop reading this document.
The other 99.99 percent of you are righteous. Not a single person on Twitter came up with the correct answer, and even I was surprised by the #3 answer. Well, even though I wrote this one myself.
The right answer is at the end of this article. Before I do that, I want to explore why Promises are so complicated and why so many people, both novices and experts, fall for them. At the same time, I will give a very unique perspective that I think will make Promises easier to understand. At the same time, I am sure that promises will not be hard to understand after knowing these things.
But before we get started, let’s understand some of the basics of Promises.
The origin of the Promises
If you read the literature on Promises, there’s one word pyramid problem that comes up a lot. It describes a state in which a large number of callbacks slowly extend to the right of the screen.
Promises do solve this problem, and it’s not just about indentation. As described in Callback Hell’s Redemption, the real problem with Callback functions is that they deprive us of the ability to use the return and throw keywords. Instead, our entire code flow is based on side effects: one function calls other functions by accident.
The description of side effects in the original text is not intuitive to understand, so I suggest you refer to WIKI. In simple terms, a function returns a value and modifies state outside the function such as global variables and so on. Virtually all asynchronous calls can be considered behavior with side effects. .
And actually, the callback is even more annoying because it breaks the stack that we normally get in most programming languages. Writing code without access to the stack is like driving without the brakes: you don’t know how important it is until you use it.
Promises give us the most important language building blocks we lose when we use asynchrony: return, throw, and stack. But promises offer these advantages if you know how to use them correctly.
Rookie mistake
Some students tried to describe Promises by using cartoons, or tried to put words to promises: “Oh, you can pass it as an asynchronous value.”
I don’t think these explanations are going to be very helpful. To me, Promises are all about code structure and process. So I think it’s better to just show some common mistakes and show how to fix them. I call these “novice questions,” which means “you’re a novice now, kid, but soon you’ll be an expert.”
“Promises” will mean different things to different people, but in this article I am specifically referring to the formal standard, which is exposed in modern browsers as Window.Promise. Not all browsers have Windows.Promise, but look for pollyfill, as Lie is the smallest standards-compliant library available.
Rookie mistake #1: The Promise version of the pyramid problem
Observing how people use large promise-style apis like PouchDB, I see a lot of bad promise usage. The most common mistake is this one:
Yes, you can actually use Promises like callbacks. Well, it’s like using a sander to cut toenails. You can actually do that.
And if you think this bug is limited to beginners, you’ll be surprised to learn that I actually saw the code above on the official blackberry developer blog. The old callback style habit is hard to kill. (To developers: Sorry for choosing your example, but your example will have positive educational significance)
The correct style is this:
This writing is called composing Promises, and it is one of the great powers of Promises. Each function will only be called after the previous Promise is called and the callback is completed, and this function will be called from the output of the previous Promise, which we’ll discuss more in this section later.
Newbie mistake #2: How to use forEach after Promises
Here is how most people begin to misunderstand Promises. Once they tried to use the forEach() loop they knew (whether it was for or while), they had no idea how to make Promises work with it. So they write code that looks something like this.
What’s wrong with this code? The problem is that the first function actually returns undefined, which means the second method doesn’t wait for all Documents to execute db.remove(). He doesn’t actually wait for anything, and may execute after any number of documents have been deleted!
This is a very subtle bug, because if PouchDB deletes these documents quickly enough, your UI will do just fine and you may not notice anything wrong at all. The bug may be exposed in some odd race issue or in some particular browser, and it may be nearly impossible to locate the problem.
In short, forEach()/for/while is not the solution you are looking for. All you need is promise.all ():
What does the above code mean? Basically, promise.all () will take an promises array as input and return a new Promise. This new promise will not return until all promises in the array have been successfully returned. It’s the asynchronous version of the for loop.
And promise.all () returns an array of execution results to the next function, useful when you want to retrieve multiple objects from PouchDB, for example. An even more useful effect is that promise.all () will return an error if any of the promises in the array return an error.
Rookie mistake #3: Forgetting to use.catch()
This is another common mistake. Many developers forget to add a.catch() to their code simply because they believe that promises will never go wrong. Unfortunately, this also means that any exceptions thrown will be eaten and you won’t be able to see them on console. This kind of problem can be very painful to debug.
Promise libraries such as Bluebird will raise an UnhandledRejectionError warning of unhandled exceptions in this scenario, which will cause script exceptions and, in Node, process crashes. It is therefore important to add.catch() correctly. .
To avoid this annoying scenario, I make a habit of using promises like this:
Even if you’re confident that no exception will occur, it’s always more prudent to add a catch(). If your assumptions turn out to be wrong, it will make your life better.
Rookie mistake #4: Using “deferred”
It’s a mistake I see so often that I don’t even want to repeat it here, as scared as Beetlejuice, the mere mention of its name conjures up more.
In short, Promises have a long and dramatic history, and the Javascript community has spent a lot of time getting them right. Early on, Deferred was introduced in the “good” libraries of Q, When, RSVP, Bluebird, Lie, etc. JQuery and Angular wrote code in this pattern before using the ES6 Promise specification.
So if you use this word in your code (AND I won’t repeat it a third time!) You’re doing it wrong. Here’s how to avoid it.
First, most Promises libraries provide a way to package a third-party object of Promises. For example, Angular’s $Q module allows you to package non-$Q Promises with $Q.hen. So Angular users can use PouchDB Promises like this.
Another strategy is to use constructors to declare schemas, which can be useful when wrapping non-Promise apis. For example, to wrap a callback style API such as Node’s fs.readfile, you can simply do this:
Finished! We beat the terrible Def…. Aha, I caught myself. 🙂
For more on why this is an anti-pattern, see Bluebird’s Promise Anti-Patterns Wiki page
Rookie mistake #5: Using side effects to call instead of returning
What’s wrong with the following code?
Well, now it’s time to talk about everything you need to know about Promises.
Seriously, this is a weird technique that, once you understand it, will avoid all the mistakes I mentioned. Are you ready?
As I said before, the magic of Promises lies in giving us the return and throw of promises. But how does this work in practice?
Each promise will provide you with a then() function (or catch(), which is really just then(null…). Grammar sugar). When we are inside the then() function:
What can we do? There are three things:
-
Return another promise
-
Return a synchronized value (or undefined)
-
Throw a synchronization exception
That’s it. Once you understand this technique, you understand Promises. So let’s take them one by one.
-
Return another promise
This is a common usage pattern in the promise documentation, known as “composing Promises “:
Notice that I’m the second promise of return, and this return is very important. If I hadn’t written return, getUserAccountById() would have been a side effect, and the next function would have received undefined instead of userAccount.
-
Return a synchronization value (or undefined)
Returning undefined is usually wrong, but returning a sync value is actually a great way to wrap sync code in promise-style code. For example, we have an in-memory cache for the Users information. We can do this:
Isn’t it great? The second function does not care whether the userAccount is taken from a synchronous or asynchronous method, and the first function is free to return a synchronous or asynchronous value.
Unfortunately, the inconvenient reality is that in JavaScript a no-return function technically returns undefined, which means you can easily inadvertently introduce side effects when you intend to return some value.
For this reason, I have a personal habit of always returning or throwing inside the then() function. I suggest you do the same.
-
Throw a synchronization exception
Talk about Throw. This is one thing that makes Promises even better. For example, we want to throw a synchronization exception when the user has logged out. This will be very simple:
Our catch() will receive a synchronous exception if the user has logged out, and will also receive an asynchronous exception in subsequent promises. Again, this function does not care whether the exception is returned synchronously or asynchronously.
This feature is very useful, so it can help locate code problems during development. For example, if we execute json.parse () anywhere within the then() function, it will throw an exception if the JSON format is wrong. This error would have been eaten with a callback style, but promises make it easier to handle in a catch() function.
Advanced error
Right, now that you know how to make Promises super easy, let’s talk about some special scenarios.
The reason I categorize these mistakes as “advanced” is because I’ve only seen them happen to developers who already know promises well. But in order to solve the puzzle at the beginning of this article, we must discuss these errors.
Advanced Error #1: Don’t knowPromise.resolve()
Promises are very useful for encapsulating synchronous and asynchronous code, as I listed above. However, if you find that you often write code like this:
You’ll find it much simpler to use promise.resolve:
It is also extremely useful for catching synchronization exceptions. Because it works so well, I’ve gotten into the habit of using it in all of my Promise-like apis:
Keep in mind that any code that throws synchronized exceptions is a potential cause for almost impossible debugging of exceptions later. But if you wrap everything in promise.resolve (), you can always use catch() to catch it later.
Similarly, there’s promise.reject (), which you can use to return a Promise that immediately returns a failure.
Advanced error #2:catch()
与 then(null, ...)
Not exactly equivalent
I said earlier that catch() is just a syntactic sugar. So the following two pieces of code are equivalent:
However, this does not mean that the following two pieces of code are equivalent:
If you’re wondering why these two pieces of code are not equivalent, consider what happens if the first function throws an exception:
Therefore, rejectHandler does not catch exceptions raised by resolveHandler when you use the form THEN (resolveHandler, rejectHandler).
Given this, my personal habit is not to apply the second argument to then(), but to always use catch(). The only exception is when I write some asynchronous Mocha test cases, where I might expect exceptions to be thrown correctly:
Having said that, Mocha and Chai make a good combination when used to test the Promise interface. The Pouchdb-plugin-seed project has some examples to get you started.
Progressive Error #3: Promises vs Promises Factories
When we want to implement a sequence of promises one by one, like promise.all () but not all promises in parallel.
You might innocently write code like this:
Unfortunately, this code will not execute as you expected, and promises you passed in executeSequentially() will still execute in parallel.
The root of this is that what you want is not really to implement a Promises sequence at all. Promises According to the rules of Promises, once a promise is created, it is carried out. So what you really need is an array of Promise Factories.
I know what you’re thinking: “What the hell Java ape is that? Why is he saying factories?” . Promises Factory: Promises Factory: Promises Factory: Promises Factory: Promises Factory: Promises Factory: Promises Factory: Promises Factory
Why is that ok? This is because a Promise Factory does not create promises until it is executed. It’s like a then function, when in fact, they’re exactly the same thing.
If you look at the executeSequentially() function above, then imagine myPromiseFactory wrapped in result.then(…). And maybe your little light bulb will go on. At this point, you know what promise is all about.
Advanced mistake #4: Well, what if I want to get two promises
Sometimes, one promise depends on another. But if we want to get the output of two promises at the same time. For example:
To be a good Javascript developer, and to avoid the pyramid problem, we might store the User object in a variable in a higher scope:
That’s fine, but PERSONALLY I think it’s a bit off-brand. My recommended strategy is to abandon stereotypes and embrace the pyramids:
. At least for the time being. Once indentation starts to become a problem, you can extract functions into a named function using a trick that Javascript developers have used since ancient times:
As your Promise code starts to get more complex, you may find yourself pulling more and more functions out into named functions, and I’ve found that by doing so, your code gets prettier and prettier, like this:
That’s what Promises are about.
Progressive mistake #5: Promises break through
In the end, this is the mistake I alluded to in the puzzle of Promises. This is a very rare use case, and probably doesn’t show up in your code at all, but it certainly blows my mind.
What do you think the following code will print?
If you think it prints bar, you’re wrong. It actually prints foo!
The reason this happens is that if you pass something like THEN () that is not a function (like promise), it actually interprets it as THEN (null), which causes the result of the previous promise to penetrate below. You can test this for yourself:
Add any number of THEN (null), and it will still print foo.
Promises vs Promise Factories. Simply put, you can pass a promise directly into the then() function, but it will not perform as you expect. Then () is expected to get a function, so you most likely want to do:
Then it will print out the bar as we expect.
So remember: always pass functions to then()!
Mystery solved
Now we know all about promsies (or close!) We should be able to solve the puzzle I proposed at the beginning of this article.
Here are all the answers to the puzzle, which I’ve presented in graphic format for you to view:
Puzzle #1
Answer:
Puzzle #2
Answer:
Puzzle #3
Answer:
Puzzle #4
Answer:
If these answers still don’t make sense to you, THEN I strongly suggest you reread this article, or implement the doSomething() and doSomethingElse() functions and try them out in a browser yourself.
Statement: In these examples, I’m assuming that doSomething() and doSomethingElse() return promises, And promises represent the end of some work outside of JavaScript event loops (e.g. IndexedDB, Network, setTimeout), which is why they sometimes seem to be executed in parallel. Here is a JSBin for simulation.
For more advanced promises, please refer to my Promise Protips Cheat Sheet
The last words about Promises
Promises are great. If you are still in callback mode, I strongly recommend switching to Promises. Your code will be smaller, more elegant, and easier to understand.
If you don’t believe me, here is proof: A refactor of PouchDB’s Map/Reduce Module, use Promises instead of callbacks. The result: 290 new lines, 555 deleted.
By the way, the guy who wrote that nasty callback code. It’s me! So this was the first time I understood the power of Promises, and I thank the other PouchDB contributors for teaching me how to do this.
Promises aren’t perfect, of course. It’s better than a callback, but that’s like saying a punch in the stomach is better than a kick in the teeth. Yes, it has a slight advantage, but if you have a choice, you’ll try to avoid both.
One proof that I had to write this blog post is that Promises are still hard to understand and easy to misuse as an improved version of the callback model. Beginners and experts alike can easily and often misuse it, and if anything, it’s not their problem. The problem is that Promises is very similar to how we write synchronous code, but not quite.
I also think Promises are hard to understand and easy to misuse. One proof of this is that I had to translate this blog post. .
To be honest, you shouldn’t have to learn a bunch of arcane rules and new apis to do the return, catch, throw, and for loops we’re so familiar with in synchronized code. You shouldn’t always have to remind yourself in your head that there are two parallel systems.
Looking forward to the async/await
This is the point I made in “Taming the Asynchronous Beast with ES7” where I explore the ASYNc/await keywords of ES7 and how they promise promises into the language. Instead of requiring us to write pseudo-synchronous code (and a fake catch() function that looks like, but is not a catch), ES7 will allow us to use real try/ catch/ return keywords, just like we learned in CS 101.
This is a great boon for the Javascript language. Because even at the end of the day, as long as our tools don’t tell us we made a mistake, these promise antipatterns keep popping up.
Given The history of JavaScript, I think it’s fair to say that JSLint and JSHint have contributed more to The community than JavaScript: The Good Parts, even though they contain virtually The same information. But the difference is being told about the mistakes you made in your code versus reading a book to understand the mistakes other people made.
The beauty of ES7 async/ await is that your errors will be flagged as syntax or compiler errors, not runtime bugs. For now, though, it’s still important to understand what Promises can do and how to use them properly in ES5 and ES6.
So while I realize that, like JavaScript: The Good Parts, this blog post is likely to have only a very limited impact, I hope you can provide it to others when you find them making The same mistake. “I have a problem with promises!”
Long press the QR code in the image recognition image (or search the wechat official account FrontEndStory) to follow the “front-end things”, which will bring you to the latest front-end technology.