User blog:Saftzie/ECMAScript 2015 Promises

17 June 2016

ECMAScript 2015, previously known as ECMAScript 6, introduced many significant features to JavaScript. One of them is a concept that has existed in several libraries for a while, including jQuery: promises.

The idea behind all of them is similar: call an asynchronous function now, but define later how to process its completion. Since MediaWiki uses jQuery, which has  to create promises, and since Internet Explorer does not support JavaScript promises (although Edge does), JavaScript promises do not have much utility at the moment within MediaWiki.

I was recently digging into them for a non-MediaWiki reason, so here are some notes on them, anyway. I developed my understanding of them by making a prototyped constructor that would be a work-alike, so this post does the same.

In the discussion below, I sometimes refer to the behavior of promises in Firefox 46 and Chrome 51.

Some definitions
Create a promise with the constructor.

Example 1: Construction pattern

The constructor argument must be a function and is called the executor. The executor runs immediately. Naming the executor is not required. It's done here for illustration only.

The arguments to the executor,  and  , are themselves functions that are passed to the executor by the promise and can accept a single argument, the resolution value or the rejection reason. The resolution and reason can be any data type.

The actions taken, depending on the state of the promise, are defined by the promise's  method.

Example 2: then

A promise can be in one of the following states: Enqueue means to spin off a separate task on the event queue.
 * fulfilled, if it immediately enqueues a task to run
 * rejected, if it immediately enqueues a task to run
 * pending, if it is neither fulfilled nor rejected

The  function supplied by the promise to the executor and the  function supplied as an argument to  are different functions with different purposes. Some sources name them both. I call them different names in this section to help keep them separate. In the code examples below, it is clear that the names are scoped differently.

A simplified model
The following example presents a model for a basic subset of a promise's capabilities the way most people would probably use them. Subsequent sections develop the model more fully.

Example 3: Simplest model

The important thing to notice is that the promise defines the values of and   after the executor runs and returns, possibly even after the executor has completed, but before  or   is enqueued.

In practice,  would signal a successful completion of the executor and would signal an error condition. However, the executor can use  and   any way that is convenient. They are simply two functions that the executor can call, depending upon the result of the asynchronous completion.

Example 4: Using the simplest model

This example outputs to the console either (approximately 2/3 of the time)  It's cows vs. pigs. Cows win! or (approximately 1/3 of the time)  It's cows vs. pigs. Pigs lose!

The invocation of  in the default   case above does nothing, because the invocation of  has already settled.

Fulfill and reject lists
The model of Promise above leaves out that  and are actually lists of functions. The promise's  method can be called multiple times to add several reactions.

Change the following code in the model to add reaction lists.

Example 5: Reaction lists

Here's an example that does not involve cows.

Example 6: Using reaction lists

This example outputs to the console either  You may have a banana. We have: 1 (or 2) Some is better than none. or  There is a problem. We have no bananas today. I never denied the involvement of bananas.

Locking-in
The executor can complete (usually asynchronously) with one of the following results: If the resolution is another promise, the promise is said to be locked-in to the other promise, taking its state from that other promise. A promise that is locked-in to another promise is pending until that promise fulfills or rejects.
 * can pass back another promise as the resolution
 * can pass back something else as the resolution to fulfill the promise
 * can pass back a reason, usually an error, why the promise can never be fulfilled

More vocabulary: Therefore a resolved promise can be fulfilled, rejected, or locked-in to another promise. Attempting to resolve an already resolved promise does nothing, except that a locking promise can settle a promise that is locked to it.
 * A promise is settled if it is either fulfilled or rejected.
 * A promise is resolved if it is settled or if it is locked-in to another promise.

 Beware that "resolve" can be used two different ways. In addition, some information on the Internet mistakenly uses "resolve" to mean "fulfill," probably because other implementations of promises, such as, use   and   as states, rather than  and.
 * Resolve can mean the executor passes back a promise or a fulfillment value via the  function.
 * Resolve can mean the promise gets locked-in to another promise by the executor or settles (including rejection) via either the  function or the   function.

The specification does not use object classes or prototype chains to detect if a proposed resolution is a promise. The specification considers any object with a callable  member to be good enough. I prefer to use the  operator.

At this point,  and   are not symmetric. Attempting to fulfill a promise with a promise would not fulfill it. However, rejecting a promise with a promise, should there ever be a reason (no pun intended) to do so, would reject it.

The specification says that if a promise resolves with the identical promise (i.e., it gets locked-in to itself), it should be rejected with an unthrown. Personally, I would think that the promise should be rejected with an unthrown , since the class of value is correct (a promise), but the actual value is invalid (itself). (ECMAScript 5.1 specifies  only applies to number types. ECMAScript 2015 allows any type.)

Firefox hangs a self-locked promise as pending forever. Chrome rejects it with  TypeError('Chaining cycle detected for promise') Neither Firefox nor Chrome (nor the specification) detects a true cycle, for example:
 * promise 1 gets locked to promise 2
 * promise 2 gets locked to promise 1

 Promises can get locked into a cycle of dependency that are impossible to settle. Any promises that lock-in to a hung promise likewise hang. Developers need to check for such dependencies and avoid them.

The model of the promise above leaves out consideration of locked-in promises. Change the following code in the model to account for locking.

Example 7: Locking-in, Part 1

If a promise becomes locked-in to a settled promise or if any other code invokes a settled promise's  method, the settled promise enqueues the fulfillment or rejection reaction from the  immediately.

Change the following code in the model to add immediate enqueuing.

Example 8: Locking-in, Part 2

There are no bananas left for this example.

Example 9: Using locked promises

This example outputs to the console (supposing the random value is 5):  locking p1 to p2 fulfilling p4 with an integer from 1 to 10 p4 fulfilled with 5 locking p2 to p3 locking p3 to p4 p3 fulfilled with 5 p2 fulfilled with 5 p1 fulfilled with 5

So, the full possible flows are as follows:
 * 1) The executor passes back a resolution via   that is not a promise.
 * 2) * The promise runs  synchronously and passes it the value from.
 * 3) *  runs   entries asynchronously.
 * 4) The executor passes back a reason via.
 * 5) * The promise runs  synchronously and passes it the reason from.
 * 6) *  runs   entries asynchronously.
 * 7) The executor passes back the same promise.
 * 8) * The promise rejects.
 * 9) The executor passes back a different promise.
 * 10) * The (locked) promise waits for the other (locking) promise to fulfill or to reject.
 * 11) * If the locking promise fulfills, the locking promise runs  from the locked promise asynchronously with its fulfillment value.
 * 12) * If the locking promise rejects, the locking promise runs  from the locked promise asynchronously with its rejection reason.
 * 13) The promise is already settled when something calls its   method.
 * 14) * If the promise was fulfilled, it runs the fulfillment reaction from  asynchronously with its fulfillment value.
 * 15) * If the promise was rejected, it runs the rejection reaction from  asynchronously with its rejection reason.

Promise.all
The  method takes an iterable (essentially an array) of promises or values and returns a new promise. The new promise fulfills if all element promises fulfill or rejects if any element promise rejects. The fulfillment value is an array of the fulfillment values of the element promises. If any element is not a promise, it is treated as a fulfillment value.

Iterable is a new type of object in ECMAScript 2015 that allows sequential access to its data. Iterables include arrays. A full examination of iterable types is outside the scope of this article. This model assumes the iterable argument is intended as an array.

Example 10: all

Promise.race
The  method takes an iterable (essentially an array) of promises or values and returns a new promise. The new promise settles if any element promise settles. The settlement result is the settlement result of the element promise. If any element is not a promise, treat it as a fulfillment value, which would cause the new promise to fulfill immediately. If the iterable argument has no elements, the new promise hangs.

As before, this model assumes the iterable argument is intended as an array.

Example 11: race

Promise.reject
The  method takes a rejection reason and returns a promise rejected with that reason.

Example 12: reject

Promise.resolve
The  method takes a value and attempts to return a promise resolved with that value. If the value is itself a promise, return the value rather than creating a new locked-in promise.

Example 13: resolve

Promise.prototype.catch
In addition to, there is also the catch method, which can define a reject reaction.

Beware that "catch" is a reserved word, so using it as an identifier may cause problems. (I'm kind of disappointed the specification did something like that.) If in doubt, quote it.

Example 14: catch