What is a Promise?
A promise is an object that may produce a single value some time in the future: either a resolved value, or a reason that it’s not resolved (e.g., a network error occurred). A promise may be in one of 3 possible states: fulfilled, rejected, or pending. Promise users can attach callbacks to handle the fulfilled value or the reason for rejection.
An Incomplete History of Promises
Early implementations of promises and futures (a similar / related idea) began to appear in languages such as MultiLisp and Concurrent Prolog as early as the 1980’s. The use of the word “promise” was coined by Barbara Liskov and Liuba Shrira in 1988.
Around the same time, Dojo added promises via the Deferred API. Growing interest and activity eventually led to the newly formed Promises/A specification designed to make various promises more interoperable.
ES6 brought a Promises/A+ compliant
Promise global, and some very important APIs were built on top of the new standard Promise support: notably the WHATWG Fetch spec and the Async Functions standard (a stage 3 draft at the time of this writing).
The promises described here are those which are compatible with the Promises/A+ specification, with a focus on the ECMAScript standard
How Promises Work
A promise is an object which can be returned synchronously from an asynchronous function. It will be in one of 3 possible states:
onFulfilled()will be called (e.g.,
onRejected()will be called (e.g.,
- Pending: not yet fulfilled or rejected
A promise is settled if it’s not pending (it has been resolved or rejected). Sometimes people use resolved and settled to mean the same thing: not pending.
Once settled, a promise can not be resettled. Calling
reject() again will have no effect. The immutability of a settled promise is an important feature.
Here is a function that returns a promise which will resolve after a specified time delay:
wait(3000) call will wait 3000ms (3 seconds), and then log
'Hello!'. All spec-compatible promises define a
.then() method which you use to pass handlers which can take the resolved or rejected value.
The ES6 promise constructor takes a function. That function takes two parameters,
reject(). In the example above, we’re only using
resolve(), so I left
reject() off the parameter list. Then we call
setTimeout() to create the delay, and call
resolve() when it’s finished.
You can optionally
reject() with values, which will be passed to the callback functions attached with
reject() with a value, I always pass an
Error object. Generally I want two possible resolution states: the normal happy path, or an exception — anything that stops the normal happy path from happening. Passing an
Error object makes that explicit.
Important Promise Rules
Promises following the spec must follow a specific set of rules:
- A promise or “thenable” is an object that supplies a standard-compliant
- A pending promise may transition into a fulfilled or rejected state.
- A fulfilled or rejected promise is settled, and must not transition into any other state.
- Once a promise is settled, it must have a value (which may be
undefined). That value must not change.
Change in this context refers to identity (
===) comparison. An object may be used as the fulfilled value, and object properties may mutate.
Every promise must supply a
.then() method with the following signature:
) => Promise
.then() method must comply with these rules:
- If the arguments supplied are not functions, they must be ignored.
onFulfilled()will be called after the promise is fulfilled, with the promise’s value as the first argument.
onRejected()may be called more than once.
.then()may be called many times on the same promise. In other words, a promise can be used to aggregate callbacks.
.then()must return a new promise,
onRejected()return a value
xis a promise,
promise2will lock in with (assume the same state and value as)
promise2will be fulfilled with the value of
- If either
onRejectedthrows an exception
promise2must be rejected with
eas the reason.
onFulfilledis not a function and
promise2must be fulfilled with the same value as
onRejectedis not a function and
promise2must be rejected with the same reason as
.then() always returns a new promise, it’s possible to chain promises with precise control over how and where errors are handled. Promises allow you to mimic normal synchronous code’s
Like synchronous code, chaining will result in a sequence that runs in serial. In other words, you can do:
Assuming each of the functions,
save() return promises,
process() will wait for
fetch() to complete before starting, and
save() will wait for
process() to complete before starting.
handleErrors() will only run if any of the previous promises reject.
Here’s an example of a complex promise chain with multiple rejections:
Note that promises have both a success and an error handler, and it’s very common to see code that does this:
But what happens if
handleSuccess() throws an error? The promise returned from
.then() will be rejected, but there’s nothing there to catch the rejection — meaning that an error in your app gets swallowed. Oops!
For that reason, some people consider the code above to be an anti-pattern, and recommend the following, instead:
The difference is subtle, but important. In the first example, an error originating in the
save() operation will be caught, but an error originating in the
handleSuccess() function will be swallowed.
In the second example,
.catch() will handle rejections from either
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:
Whatever you prefer, I recommend ending all promise chains with a
.catch(). That’s worth repeating:
I recommend ending all promise chains with a
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
.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
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
onCancelcleanup 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
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
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
- 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.
Eric Elliott is a tech product and platform advisor, author of “Composing Software”, cofounder of EricElliottJS.com and DevAnywhere.io, and dev team mentor. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He enjoys a remote lifestyle with the most beautiful woman in the world.