In the second example, .catch() will handle rejections from either save(), or handleSuccess().
Of course, the save() error might be a networking error, whereas the handleSuccess() error may be because the developer forgot to handle a specific status code. What if you want to handle them differently? You could opt to handle them both:
save() .then( handleSuccess, handleNetworkError ) .catch(handleProgrammerError);
Whatever you prefer, I recommend ending all promise chains with a .catch(). That?s worth repeating:
I recommend ending all promise chains with a .catch().
How Do I Cancel a Promise?
One of the first things new promise users often wonder about is how to cancel a promise. Here?s an idea: Just reject the promise with ?Cancelled? as the reason. If you need to deal with it differently than a ?normal? error, do your branching in your error handler.
Here are some common mistakes people make when they roll their own promise cancellation:
Adding .cancel() to the promise
Adding .cancel() makes the promise non-standard, but it also violates another rule of promises: Only the function that creates the promise should be able to resolve, reject, or cancel the promise. Exposing it breaks that encapsulation, and encourages people to write code that manipulates the promise in places that shouldn’t know about it. Avoid spaghetti and broken promises.
Forgetting to clean up
Some clever people have figured out that there?s a way to use Promise.race() as a cancellation mechanism. The problem with that is that cancellation control is taken from the function that creates the promise, which is the only place that you can conduct proper cleanup activities, such as clearing timeouts or freeing up memory by clearing references to data, etc…
Forgetting to handle a rejected cancel promise
Did you know that Chrome throws warning messages all over the console when you forget to handle a promise rejection? Oops!
The withdrawn TC39 proposal for cancellation proposed a separate messaging channel for cancellations. It also used a new concept called a cancellation token. In my opinion, the solution would have considerably bloated the promise spec, and the only feature it would have provided that speculations don?t directly support is the separation of rejections and cancellations, which, IMO, is not necessary to begin with.
Will you want to do switching depending on whether there is an exception, or a cancellation? Yes, absolutely. Is that the promise?s job? In my opinion, no, it?s not.
Rethinking Promise Cancellation
Generally, I pass all the information the promise needs to determine how to resolve / reject / cancel at promise creation time. That way, there?s no need for a .cancel() method on a promise. You might be wondering how you could possibly know whether or not you?re going to cancel at promise creation time.
?If I don?t yet know whether or not to cancel, how will I know what to pass in when I create the promise??
If only there were some kind of object that could stand in for a potential value in the future? oh, wait.
The value we pass in to represent whether or not to cancel could be a promise itself. Here?s how that might look:
We?re using default parameter assignment to tell it not to cancel by default. That makes the cancel parameter conveniently optional. Then we set the timeout as we did before, but this time we capture the timeout?s ID so that we can clear it later.
We use the cancel.then() method to handle the cancellation and resource cleanup. This will only run if the promise gets cancelled before it has a chance to resolve. If you cancel too late, you?ve missed your chance. That train has left the station.
Note: You may be wondering what the noop() function is for. The word noop stands for no-op, meaning a function that does nothing. Without it, V8 will throw warnings: UnhandledPromiseRejectionWarning: Unhandled promise rejection. It?s a good idea to always handle promise rejections, even if your handler is a noop().
Abstracting Promise Cancellation
This is fine for a wait() timer, but we can abstract this idea further to encapsulate everything you have to remember:
- Reject the cancel promise by default ? we don?t want to cancel or throw errors if no cancel promise gets passed in.
- Remember to perform cleanup when you reject for cancellations.
- Remember that the onCancel cleanup might itself throw an error, and that error will need handling, too. (Note that error handling is omitted in the wait example above ? it?s easy to forget!)
Let?s create a cancellable promise utility that you can use to wrap any promise. For example, to handle network requests, etc? The signature will look like this:
speculation(fn: SpecFunction, shouldCancel: Promise) => Promise
The SpecFunction is just like the function you would pass into the Promise constructor, with one exception ? it takes an onCancel() handler:
SpecFunction(resolve: Function, reject: Function, onCancel: Function) => Void
Note that this example is just an illustration to give you the gist of how it works. There are some other edge cases you need to take into consideration. For example, in this version, handleCancel will be called if you cancel the promise after it is already settled.
I?ve implemented a maintained production version of this with edge cases covered as the open source library, Speculation.
Let?s use the improved library abstraction to rewrite the cancellable wait() utility from before. First install speculation:
npm install –save speculation
Now you can import and use it:
This simplifies things a little, because you don?t have to worry about the noop(), catching errors in your onCancel(), function or other edge cases. Those details have been abstracted away by speculation(). Check it out and feel free to use it in real projects.
Extras of the Native JS Promise
The native Promise object has some extra stuff you might be interested in:
- Promise.reject() returns a rejected promise.
- Promise.resolve() returns a resolved promise.
- Promise.race() takes an array (or any iterable) and returns a promise that resolves with the value of the first resolved promise in the iterable, or rejects with the reason of the first promise that rejects.
- Promise.all() takes an array (or any iterable) and returns a promise that resolves when all of the promises in the iterable argument have resolved, or rejects with the reason of the first passed promise that rejects.
For instance, if you?re using Redux, I suggest that you check out redux-saga: A library used to manage side-effects in Redux which depends on async functions throughout the documentation.
I hope even experienced promise users have a better understanding of what promises are and how they work, and how to use them better after reading this.
Explore the Series
- What is a Closure?
- What is the Difference Between Class and Prototypal Inheritance?
- What is a Pure Function?
- What is Function Composition?
- What is Functional Programming?
- What is a Promise?
- Soft Skills
- Barbara Liskov; Liuba Shrira (1988). ?Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems?. Proceedings of the SIGPLAN ?88 Conference on Programming Language Design and Implementation; Atlanta, Georgia, United States, pp. 260?267. ISBN 0?89791?269?1, published by ACM. Also published in ACM SIGPLAN Notices, Volume 23, Issue 7, July 1988.
He enjoys a remote lifestyle with the most beautiful woman in the world.