19 Jul 2015
First, we’ll give an idea of what promises are good for and then go over simple examples to understand how promises work and also cover some extra methods from the API.
Google Chrome implements the Promises/A+ standard . The code snippets in this post were test on the regular Chrome, version 43.
Promises are an abstraction to make working with asynchronous code in a more expressive manner . In synchronous code, we think in terms of return’s for normal execution and throw’s for exceptions. In asynchronous world, the code flow is structured around callbacks, for example, onSuccess or onFailure callbacks.
As a toy example, consider the case where we have to call 3 functions, each depending on the previous one, but each of them can fail for whatever reason and we need to handle exceptions. In the synchronous case, it’s straightforward:
If these functions are asynchronous, we’d have to handle these dependencies via the callbacks, creating a nested set of calls (aka callback hell):
We could definitely work around the depth of calls by using auxiliary functions, but Promises make use cases like this easier:
In this case, we’d have to change the functions to return promise objects.
We’ll next cover small examples exploring the behavior of promises to understand how they work.
Creating a promise
The promise constructor expects a callback (also called executor). This callback on its turn expects takes two other functions as arguments,
resolve() - to be called when a normal execution is ended - and
reject() - called when an exception occurs. A sample executor example could be:
which succeeds half of the time and fails the other half. We then use this function to create a new promise object:
Calling a promise
After instantiating a promise, we can call the
then() method from the promise, passing two functions, which we’ll refer to onFulfill and onReject. The
onFulfill() function will be called when the promise invokes
resolve() is called and the
onReject() function one when
reject() is called.
Now that we saw the basic syntax for creating and handling promises, let’s delve into more details.
Resolving a promise
When we pass a function (executor) to instantiate a promise, it gets immediately executed. For example, in the code below, it will print “running executor” first and then “constructed”.
In the example above we’re not using neither resolve or reject. If we can include a call to
resolve() and then invoke the
We’ll see that the messages are printed in this order:
Note that the
resolve() function was called when constructing the promise, but the action was deferred until we passed a callback to the
then() method. If we think in terms of events, we set up the listener after the event was fired, but the action of the listener was fired nevertheless.
When a promise is first created, it’s in a state called pending. When the
resolve() function is called, it changes its state to fulfilled, when
reject() is called, it changes its state to rejected. When a promise is either fulfilled or rejected, we say it’s settled.
In the example above, by the time we called
then(), the promise was already fulfilled, so the action fired right away.
We can simulate calling
then() while the promise is pending, that is, before
reject() is called, by using the
The messages are now printed in this order:
Here the promise was in the pending state and then after a second it got fulfilled, so the callback passed to
then() was fired.
With events and listeners, we have to guarantee an event is not fired before the listener is setup, otherwise we have to store that event and process later. This is one problem promise solves for us, as pointed out by .
Reactions can be queued
Note that the
then() function can be called multiple times for the same promise and each time the callbacks passed to it are enqueued and when the promise is settled, they fire in order. For example:
We’ll see both callbacks passed to then are invoked:
Promises can’t be unsettled
reject() calls only take effect if the promise is in a pending state. Remember that the first time we call
reject() the promise changes its state from pending, so all subsequent calls to
reject() will be ignored:
then() method of a promise returns another promise. The value of the promise is the value returned by the handling functions passed to
then(). If the handling functions executed normally, then the returned promise is fulfilled with that same value. If the handling function throws, the returned promise is reject with the error thrown.
An example of a the normal execution:
It’s as if
nextPromise was created with:
Here’s another example where the promise handlers throws exceptions:
Resolving a promise with another promise
So far we’ve been using numbers as arguments to the
then() method - which is also called thenable).
Suppose we have a promise A in which we call
resolve() with another promise
B. When we call
A.then() the the value passed to the
onFulfill() callback of the
then() method will be the value resolved from the promise
Consider the following example:
outerPromise calls resolve with another promise that resolves with a number. The
onFulfill() callback passed to
then() will receive 10.
Chaining promises with promises
Remember that when the callbacks provided to
then() returns normally, the value is used to create another promise which automatically calls
resolve() with that value. If that vale is a promise, then the value provided to
onFulfill() of the next
then() will be the value from
resolve(), which in this case will be 11.
Other methods from the API
So far we’ve considered only promises that need to be executed sequentially. For those that can be executed in parallel, the Promise class contains the
all() method, which will take an array of promises and returns a new promise, which will wait until all input promises are settled. If they were all fulfilled, then it will can resolve with an array with the resolve values. If any of them if rejected, it will call
reject() with the error of the first promised to be rejected.
In our examples, we mostly exclusively focused on the “normal” execution flow, in which
onFulfill() are called. The exception case is very similar, with the
onReject() functions being called. One difference is that
reject() might be triggered implicitly, for example if an exception is thrown within a promise or one of the reactions callbacks.
If we want, we can only provide the
onFulfill() callback to the
then() method, but if we want to provide only the
onReject(), we’ll need to pass an empty function as
onFulfill(). To cover this case, promises also have the
catch() which does essentially this.
We saw that promises can be used to make code dealing with callback more legible and easier to reason about. There are some complexities encapsulate in promise objects and its methods, so it can be a bit daunting to understand how they work behind the scenes. We covered some small examples and proceeded in steps to make it easier to digest.
then() method behavior.