FANDOM


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 jQuery.Deferred 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
// make a promise with the executor
// execute something asynchronously
// pass back a result based on some condition
promise = new Promise(function executor(resolve, reject) {
	// ...
	if (someResultCondition) {
		resolve(resolution);
	} else {
		reject(reason);
	}
	// ...
});

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, resolve and reject, 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 then method.

Example 2: then()
promise.then(fulfillReaction, rejectReaction);

A promise can be in one of the following states:

  • fulfilled, if it immediately enqueues a task to run fulfillReaction
  • rejected, if it immediately enqueues a task to run rejectReaction
  • pending, if it is neither fulfilled nor rejected

Enqueue means to spin off a separate task on the event queue.

The reject function supplied by the promise to the executor and the rejectReaction function supplied as an argument to then are different functions with different purposes. Some sources name them both reject. 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
function Promise(executor) {
	'use strict';
 
	var
		PENDING = 1,
		FULFILLED = 2,
		REJECTED = 3,
		state = PENDING,
		fulfillReaction = null,
		rejectReaction = null;
 
	// resolve the promise with a fulfillment value
	function resolve(value) {
		if (state === PENDING) {
			state = FULFILLED;
			if (fulfillReaction) {
				setTimeout(fulfillReaction, 0, value);
			}
			fulfillReaction = null; // free them
			rejectReaction = null;
		}
	}
 
	// resolve the promise with a rejection
	function reject(reason) {
		if (state === PENDING) {
			state = REJECTED;
			if (rejectReaction) {
				setTimeout(rejectReaction, 0, reason);
			}
			fulfillReaction = null; // free them
			rejectReaction = null;
		}
	}
 
	// then is a privileged method here instead of a prototyped one
	//   so it can access private members fulfillReaction and rejectReaction
	// the native JavaScript constructor does not have this limitation
	this.then = function (fulfill, reject) {
		if (typeof fulfill === 'function') {
			fulfillReaction = fulfill;
		}
		if (typeof reject === 'function') {
			rejectReaction = reject;
		}
	};
 
	if (typeof executor !== 'function') {
		throw new TypeError('Argument of Promise constructor must be callable');
	}
	executor(resolve, reject);
}

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

In practice, resolve would signal a successful completion of the executor and reject would signal an error condition. However, the executor can use resolve and reject 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
// cows and pigs are passed from the constructor
// and are scoped inside the anonymous executor function
p = new Promise(function (cows, pigs) {
	// use setTimeout as a trivial asynchronous example
 	setTimeout(function () {
 		switch (Math.floor(3 * Math.random())) {
 		case 0:
 			cows();
 			break;
 		case 1:
 			pigs();
 			break;
 		default:  // case 2
 			cows();
 			pigs(); // this call does nothing
 		}
 	console.log('It\'s cows vs. pigs.');
 	});
});
 
p.then(function () { // define the reaction to cows
	console.log('Cows win!');
}, function () {     // define the reaction to pigs
	console.log('Pigs lose!');
});

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 pigs in the default switch case above does nothing, because the invocation of cows has already fulfilled p.

Fulfill and reject lists

The model of Promise above leaves out that fulfillReaction and rejectReaction are actually lists of functions. The promise's then 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
	var
		PENDING = 1,
		FULFILLED = 2,
		REJECTED = 3,
		state = PENDING,
		fulfillReactions = [],
		rejectReactions = [],
		i;
 
	// resolve the promise with a fulfillment value
	function resolve(value) {
		if (state === PENDING) {
			state = FULFILLED;
			for ( i = 0; i < fulfillReactions.length; ++i ) {
				setTimeout(fulfillReactions[i], 0, value);
			}
			fulfillReactions = null; // free them
			rejectReactions = null;
		}
	}
 
	// resolve the promise with a rejection
	function reject(reason) {
		if (state === PENDING) {
			state = REJECTED;
			for ( i = 0; i < rejectReactions.length; ++i ) {
				setTimeout(rejectReactions[i], 0, reason);
			}
			fulfillReactions = null; // free them
			rejectReactions = null;
		}
	}
 
	// then is a privileged method here instead of a prototyped one
	//   so it can access private members fulfillReactions and rejectReactions
	// the native JavaScript constructor does not have this limitation
	this.then = function (fulfill, reject) {
		if (typeof fulfill === 'function') {
			fulfillReactions.push(fulfill);
		}
		if (typeof reject === 'function') {
			rejectReactions.push(reject);
		}
		return this; // chainable
	};

Here's an example that does not involve cows.

Example 6: Using reaction lists
p = new Promise(function (resolve, reject) {
	setTimeout(function () {
		var
			banana = Math.floor(3 * Math.random());
 
		if (banana > 0) {
			resolve(banana);
		} else {
			reject('We have no bananas today.');
		}
	});
});
 
p.then(function () {
	console.log('You may have a banana.');
}, function () {
	console.log('There is a problem.');
});
 
p.then(function (value) {
	console.log('We have:', value);
}, function (reason) {
	console.log(reason);
});
 
p.then(function () {
	console.log('Some is better than none.');
});

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:

  • resolve can pass back another promise as the resolution
  • resolve can pass back something else as the resolution to fulfill the promise
  • reject can pass back a reason, usually an error, why the promise can never be fulfilled

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.

More vocabulary:

  • 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.

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.

Beware that "resolve" can be used two different ways.

  • Resolve can mean the executor passes back a promise or a fulfillment value via the resolve function.
  • Resolve can mean the promise gets locked-in to another promise by the executor or settles (including rejection) via either the resolve function or the reject function.

In addition, some information on the Internet mistakenly uses "resolve" to mean "fulfill," probably because other implementations of promises, such as jQuery.Deferred, use resolved and rejected as states, rather than fulfilled and rejected.

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

At this point, resolve and reject 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 TypeError. Personally, I would think that the promise should be rejected with an unthrown RangeError, since the class of value is correct (a promise), but the actual value is invalid (itself). (ECMAScript 5.1 specifies RangeError 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
	var
		PENDING = 1,
		FULFILLED = 2,
		REJECTED = 3,
		self = this, // save this to use in privileged methods
		state = PENDING,
		resolved = false,
		fulfillReactions = [],
		rejectReactions = [],
		result, i;
 
	// handle the event of the promise being fulfilled
	function onFulfilled(value) {
		if (state === PENDING) {
			state = FULFILLED;
			result = value; // save in case anything 'then's after settling
			for ( i = 0; i < fulfillReactions.length; ++i ) {
				setTimeout(fulfillReactions[i], 0, value);
			}
			fulfillReactions = null; // free them
			rejectReactions = null;
		}
	}
 
	// handle the event of the promise being rejected
	function onRejected(reason) {
		if (state === PENDING) {
			state = REJECTED;
			result = reason; // save in case anything 'then's after settling
			for ( i = 0; i < rejectReactions.length; ++i ) {
				setTimeout(rejectReactions[i], 0, reason);
			}
			fulfillReactions = null; // free them
			rejectReactions = null;
		}
	}
 
	// resolve the promise with a proposed resolution
	function resolve(resolution) {
		if (!resolved) {
			resolved = true;
			if (resolution === self) {
				// a promise cannot be locked to itself
				onRejected(new TypeError('Promise must not self-resolve'));
			} else if (resolution instanceof Promise) {
				// lock this promise to the resolution promise by 'then'ing
				// async to make 'then' evaluate after this task completes
				setTimeout(resolution.then, 0, onFulfilled, onRejected);
			} else {
				onFulfilled(resolution);
			}
		}
	}
 
	// resolve the promise with a rejection
	function reject(reason) {
		if (!resolved) {
			resolved = true;
			onRejected(reason);
		}
	}

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

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

Example 8: Locking-in, Part 2
	// then is a privileged method here instead of a prototyped one
	//   so it can access private members
	//   state, fulfillReactions, rejectReactions, and result
	// the native JavaScript constructor does not have this limitation
	this.then = function (fulfill, reject) {
		if (typeof fulfill === 'function') {
			if (state === PENDING) {
				fulfillReactions.push(fulfill); // save it for later
			} else if (state === FULFILLED) {
				setTimeout(fulfill, 0, result); // enqueue it immediately
			}
		}
		if (typeof reject === 'function') {
			if (state === PENDING) {
				rejectReactions.push(reject);  // save it for later
			} else if (state === REJECTED) {
				setTimeout(reject, 0, result); // enqueue it immediately
			}
		}
		return this; // chainable
	};

There are no bananas left for this example.

Example 9: Using locked promises
// lock p1 to pending p2 immediately
p1 = new Promise(function (resolve, reject) {
	setTimeout(function () {
		console.log('locking p1 to p2');
		resolve(p2);
	});
});
 
p1.then(function (v) {
	console.log('p1 fulfilled with', v);
});
 
// lock p2 to pending p3 after 1 sec
p2 = new Promise(function (resolve, reject) {
	setTimeout(function () {
		console.log('locking p2 to p3');
		resolve(p3);
	}, 1000);
});
 
p2.then(function (v) {
	console.log('p2 fulfilled with', v);
});
 
// lock p3 to fulfilled p4 after 2 sec
p3 = new Promise(function (resolve, reject) {
	setTimeout(function () {
		console.log('locking p3 to p4');
		resolve(p4);
	}, 2000);
});
 
p3.then(function (v) {
	console.log('p3 fulfilled with', v);
});
 
// fulfill p4 immediately
p4 = new Promise(function (resolve, reject) {
	setTimeout(function () {
		console.log('fulfilling p4 with an integer from 1 to 10');
		resolve(Math.floor(10 * Math.random()) + 1);
	});
});
 
p4.then(function (v) {
	console.log('p4 fulfilled with', v);
});

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 resolve that is not a promise.
    • The promise runs onFulfilled synchronously and passes it the value from resolve.
    • onFulfilled runs fulfillReactions entries asynchronously.
  2. The executor passes back a reason via reject.
    • The promise runs onRejected synchronously and passes it the reason from reject.
    • onRejected runs rejectReactions entries asynchronously.
  3. The executor passes back the same promise.
    • The promise rejects.
  4. The executor passes back a different promise.
    • The (locked) promise waits for the other (locking) promise to fulfill or to reject.
    • If the locking promise fulfills, the locking promise runs onFulfilled from the locked promise asynchronously with its fulfillment value.
    • If the locking promise rejects, the locking promise runs onRejected from the locked promise asynchronously with its rejection reason.
  5. The promise is already settled when something calls its then method.
    • If the promise was fulfilled, it runs the fulfillment reaction from then asynchronously with its fulfillment value.
    • If the promise was rejected, it runs the rejection reaction from then asynchronously with its rejection reason.

More members

Promise.all

The all 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()
// take an array of promises and return a promise
// fulfill with an array of values if all element promises fulfill
// reject with the reason if any element promise rejects
Promise.all = function (array) {
	if (Object.prototype.toString.call(array) !== '[object Array]') {
		throw new TypeError('Argument of Promise.all is not an array');
	}
	return new Promise(function (resolve, reject) {
		var
			count = array.length,
			result = [],
			i;
 
		// make a reaction that sets result[i]
		// fulfill if all reactions have run
		function resolveElement(i) {
			var
				pending = true;
 
			// make a closure with i & pending
			return function (value) {
				if (pending) {
					pending = false;
					result[i] = value;
					if (--count === 0) {
						resolve(result);
					}
				}
			};
		}
 
		for ( i = 0; i < array.length ; ++i ) {
			if (array[i] instanceof Promise) {
				result[i] = undefined;
				array[i].then(resolveElement(i), reject);
			} else {
				// set the result as if it were a fulfilled Promise
				result[i] = array[i];
				--count;
			}
		}
		if (count === 0) { // Promise.all has no dependencies
			setTimeout(resolve, 0, result);
		}
	});
};

Promise.race

The race 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()
// take an array of promises and return a promise
// fulfill with the value if any element promise fulfills
// reject with the reason if any element promise rejects
Promise.race = function (array) {
	if (Object.prototype.toString.call(array) !== '[object Array]') {
		throw new TypeError('Argument of Promise.race is not an array');
	}
	return new Promise(function (resolve, reject) {
		var
			i;
 
		for ( i = 0; i < array.length ; ++i ) {
			if (array[i] instanceof Promise) {
				array[i].then(resolve, reject);
			} else {
				// fulfill as if it were a fulfilled Promise
				setTimeout(resolve, 0, array[i]);
			}
		}
	});
};

Promise.reject

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

Example 12: reject()
// return a new promise which is rejected for the given reason
Promise.reject = function (reason) {
	return new Promise(function (_, reject) {
		setTimeout(reject, 0, reason);
	});
};

Promise.resolve

The resolve 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()
// if value is a promise, return the promise
// otherwise return a new promise which is fulfilled with the given value
Promise.resolve = function (value) {
	if (value instanceof Promise) {
		return value;
	}
	return new Promise(function (resolve, _) {
		setTimeout(resolve, 0, value);
	});
};

Promise.prototype.catch

In addition to then, 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()
// shortcut to define just a reject reaction
Promise.prototype['catch'] = function (reject) {
	return this.then(null, reject);
};

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.