Dojo Deferreds and Promises

Deferreds are a wonderful and powerful thing, but they're really an implementation of something greater - a promise. In this tutorial, we'll cover what that means, as well as some additional pieces of Dojo's API to work with both promises and regular values in a uniform way.

  • Difficulty: Intermediate
  • Dojo Version: 1.6

Getting Started

Now that you've learned about dojo.Deferred and the basic concepts related to it, we're going to introduce you to one that's a bit more abstract: promises. A promise is an object that represents the eventual value returned from the completion of an operation. A promise has the following characteristics:

  • Can be in one of three states: unfulfilled, resolved, rejected
  • May only change from unfulfilled to resolved or unfulfilled to rejected
  • Implements a then method for registering callbacks for notification of state change
  • Callbacks cannot change the value produced by the promise
  • A promise's then method returns a new promise, to provide chaining while keeping the original promise's value unchanged

With this knowledge, let's discover how Dojo implements promises.

dojo.Deferred as a Promise

If a promise sounds a lot like a dojo.Deferred, then you've been paying attention. In fact, dojo.Deferred is Dojo's main implementation of the promise API. Let's look at the chaining example from the last tutorial in the context that dojo.Deferred is a promise:

// original is a dojo.Deferred
var original = dojo.xhrGet({
	url: "users-mangled.json",
	handleAs: "json"
});

As we said before, dojo.xhrGet (and all of Dojo's Ajax helpers) returns a dojo.Deferred. You could say that this dojo.Deferred represents the eventual value returned from the completion of the request from the server. Initially, it will be in an unfulfilled state and will change to either resolved or rejected depending on the result from the server.

Given the resulting Deferred from our xhr call, we can register callbacks via the then method. However, we didn't thoroughly cover what then returns, but only that the return value has a then method. You might have thought that it returns the original Deferred, but it really returns a simple object that fulfills the promise API: the only two methods on this object are then and cancel (to indicate that the result is no longer needed). Let's look:

// result is a new promise that produces a
// new value
var result = original.then(function(res){
	var userlist = dojo.byId("userlist1");

	return dojo.map(res, function(user){
		dojo.create("li", {
			innerHTML: dojo.toJson(user)
		}, userlist);

		return {
			id: user[0],
			username: user[1],
			name: user[2]
		};
	});
});

This call to then produces a promise object whose value will be set by the return value of the callback function. We can see that the value produced by this new promise is different than that of the original dojo.Deferred by calling the promise's then method:

// chaining to the result promise rather than
// the original deferred to get our new value
result.then(function(objs){
	var userlist = dojo.byId("userlist2");

	dojo.forEach(objs, function(user){
		dojo.create("li", {
			innerHTML: dojo.toJson(user)
		}, userlist);
	});
});

The value of the promise returned from then is always the return value of the callback — if you didn't return a value, the promise's value will be undefined! If you're seeing a random undefined somewhere in your chain, check to make sure that you're providing proper return values in your callbacks. If you don't care about chaining, it's not important to worry about returning a value.

We can also check that the original dojo.Deferred's value is unchanged as well:

// creating a list to show that the original
// deferred's value was untouched
original.then(function(res){
	var userlist = dojo.byId("userlist3");

	dojo.forEach(res, function(user){
		dojo.create("li", {
			innerHTML: dojo.toJson(user)
		}, userlist);
	});
});
View Demo

As we saw before, chaining is powerful; it's even more powerful when you know that each link in the chain is immutable.

It should also be noted that dojo.Deferred contains another important property: promise. This is an object that only implements the promise API, but represents the value that the dojo.Deferred will produce. The promise property allows you to minimize side-effects from consumers of your API by preventing someone from calling resolve or reject by accident (or on purpose), but will still allow them to get the value of the original dojo.Deferred.

dojo.when

Now that we understand what a promise is and why it's useful, let's talk about dojo.when. It is a powerful function that Dojo provides that allows you to handle either promises or standard values using a consistent API.

The dojo.when method takes up to four arguments: a promise or value, a callback, an optional error handler, and an optional progress handler. It takes one of two paths:

  • If the first argument is not a promise, the callback will be called immediately with the provided value as the first argument, and the result of the callback will be returned.
  • If the first argument is a promise, the callback, error handler, and progress handler are passed to the promise's then method, and the resulting promise is returned, setting up your callback to execute when the promise is ready.

Let's revisit our getUserList function from the Deferred tutorial:

function getUserList(){
	return dojo.xhrGet({
		url: "users-mangled.json",
		handleAs: "json"
	}).then(function(res){
		return dojo.map(res, function(user){
			return {
				id: user[0],
				username: user[1],
				name: user[2]
			};
		});
	});
}

Let's say that the list of users won't change very often and can be cached on the client instead of fetching them every time this function is called. In this case, because dojo.when takes either a value or a promise, getUserList could be changed to return either a promise or an array of users, and we can then handle the return value with dojo.when:

var getUserList = (function(){
	var users;
	return function(){
		if(!users){
			return dojo.xhrGet({
				url: "users-mangled.json",
				handleAs: "json"
			}).then(function(res){
				// Save the resulting array into the users variable
				users = dojo.map(res, function(user){
					return {
						id: user[0],
						username: user[1],
						name: user[2]
					};
				});

				// Make sure to return users here,
				// for valid chaining
				return users;
			});
		}
		return users;
	};
})();

dojo.when(getUserList(), function(users){
	// This callback will be run after the request completes

	var userlist = dojo.byId("userlist1");
	dojo.forEach(users, function(user){
		dojo.create("li", {
			innerHTML: dojo.toJson(user)
		}, userlist);
	});

	dojo.when(getUserList(), function(user){
		// This callback will be run right away since it's already in the cache

		var userlist = dojo.byId("userlist2");
		dojo.forEach(users, function(user){
			dojo.create("li", {
				innerHTML: dojo.toJson(user)
			}, userlist);
		});
	});
});
View Demo

It also could be that you're in charge of the API for creating the user list, and want a clean API for your developers to hand you a list of users from either the server (via a Deferred) or an array. In this case, you might come up with a function similar to the following:

function createUserList(node, users){
	node = dojo.byId(node);

	return dojo.when(
		users,
		function(users){
			dojo.forEach(users, function(user){
				dojo.create("li", {
					innerHTML: dojo.toJson(user)
				}, node);
			});
		},
		function(error){
			dojo.create("li", {
				innerHTML: "Error: " + error
			}, node);
		}
	);
}

var users = dojo.xhrGet({
	url: "users-mangled.json",
	handleAs: "json"
}).then(function(res){
	return dojo.map(res, function(user){
		return {
			id: user[0],
			username: user[1],
			name: user[2]
		};
	});
});

createUserList("userlist1", users);
createUserList("userlist2",
	[{ id: 100, username: "username100", name: "User 100" }]);
View Demo

As you can see, dojo.when allows developers to elegantly handle both synchronous and asynchronous usecases with one API, on both the producer and consumer ends of the spectrum.

Conclusion

The addition of the promises API to Dojo allows developers the opportunity to create more powerful applications in two ways: side-effects are avoided because of the guarantee of immutability of promises from functions like dojo.Deferred, while dojo.when provides an API for bridging the gap between promise-based and value-based coding.

Error in the tutorial? Can’t find what you are looking for? Let us know!