when

Brian Cavalier edited this page Nov 14, 2012 · 16 revisions

Might-be-a-promise

One benefit of the when() function is that it can help you write code that deals with cases where you may have a promise or a value, but you can't be sure which. Huh? For example, Dojo's excellent dojo/store modules define a standard API for datastores. One implementation of that is JsonRest, which uses XHR to retrieve data from a REST endpoint, and returns a promise that resolves with the fetched data once the XHR has completed. Another implementation is Memory, which is basically a hashtable, and returns actual results immediately, rather than promises.

Because they implement the same API, it can be extremely useful to use Memory for unit testing code that normally uses JsonRest in production. Take the following code, for example, that issues a query for all employees, and then displays them.

function displayAllEmployees() {
	employeeStore.query({}).then(
		function(allEmployees) {
			allEmployees.forEach(displayOneEmployee);
		},
		handleError
	);
}

If employeeStore is a JsonRest, this code works just fine, since its .query() method returns a promise. However, if we substitute Memory during unit testing, this code will break because its query() returns an Array, which obviously does not have a then() method.

If we rewrite displayAllEmployees() to use when(), it will work in either case because when() can accept either a promise or a value.

function displayAllEmployees() {
	when(employeeStore.query({}),
		function(allEmployees) {
			allEmployees.forEach(displayOneEmployee);
		},
		handleError
	);
}

You could obviously write code to test the return value of employeeStore.query(), but you'd have to write that same code everywhere. You could also wrap that code up in a function ... but then you will have written the when() function (at least partly, read on), so why not just use when.js's when()?

NOTE: when() certainly is not a magic bullet for smoothing out differences between asynchronous and synchronous code, but in many cases, it can be extremely helpful.

Non-compliant promises

If all Promises/A compliant promises have a .then(callback, errback) method, what's the point of when()? Can't we just call .then() on any promise we get our hands on and be done with it?

Ideally, yes, but in reality, there are promise implementations that pass the Promises/A duck-type test, but that don't fully implement the specified behaviors, such as chaining and forwarding. Two examples of such promises are jQuery's Deferred[1] prior to 1.8[3], and the promises returned by curl's global API[1].

So, calling a method from a 3rd party lib that returns a promise may still get you into trouble if you simply call its .then() and expect it to work like a Promises/A promise. For example, it may not forward results, as is the case with jQuery Deferred[1][3].

To deal with these situations, when.js's when() function assimilates promises that pass the duck-type test, but that may not be Promises/A compliant. More specifically, when() trusts only when.js promises, and does not trust foreign promises. Practically speaking, if you pass a foreign-but-Promises/A-compliant promise (e.g. Dojo's Deferred, Q, promised-io, uber.js, etc.) to when() you will not notice a difference--it will behave as usual, with all the Promises/A goodness.

Here's the important part: if you pass a non-compliant promise to when(), it will return a fully compliant promise, so any downstream .then()s will have Promises/A goodness.

// It's easy to get a compliant promise from a non-compliant one

// when 1.4.0 or later
var compliant = when.resolve(methodThatReturnsNonCompliantPromise());

// when < 1.4.0, but also works in when 1.4.0
var compliant = when(methodThatReturnsNonCompliantPromise());

So, by using when(), you can take advantage of Promises/A features, like forwarding, without worrying about whether the promise provider (methodThatReturnsNonCompliantPromise() in this case) returns compliant promises or not. In other words, you don't have to think about it, just use when().

// Even though we're supplying a non-compliant promise, all subsequent
// .then()s will be compliant, and thus have Promises/A forwarding goodness.
when(methodThatReturnsNonCompliantPromise(),
	function(result) {
		return getTransformedResult(result);
	}
).then(
	function(transformedResult) {
		// We'll get the forwarded, transformed result here
		return transformItAgain(transformedResult);
	}
).then(
	function(twiceTransformedResult) {
		// We'll get the forwarded, transformed result here
		doSomethingCool(twiceTransformedResult);
	}
);

Here is an example of how pre-1.8 jQuery Deferred[3] behaved when used alone, and when used with when.js's when(). Notice that forwarding works correctly with when(). Also notice that even a simple-minded "fake" promise can be made to behave with when().

Obviously, when() can't solve every problem[2], but using when() can normalize promise behavior when using promises from various sources.

Notes

[1]: It's important to note that there's nothing necessarily wrong with these promise implementations. They have different goals, and thus have different priorities. For example, jQuery Deferred provides some nice conveniences such as .always(), and curl's promises provide a finer-grained error handling opportunity than other strategies which might, for example, use a global error handler callback.

[2]: If you supply a completely broken promise, when() can't necessarily make it behave--but then, I'd hesitate to call that broken thing a promise at all :)

[3]: As of 1.8, jQuery Deferred's then() does support forwarding, but is still not fully Promises/A compliant in how it handles rejections and exceptions. See this fiddle, which shows when.js, Q, and jQuery behaviors for comparison.