Understanding Deferreds and Promises in Dojo

By on March 6, 2015 11:18 am

It’s been a while since we’ve dove into Dojo’s Deferred module on the SitePen blog—the last time was back in 2010 when we introduced the promise-inspired improvements that landed with Dojo 1.5. Quite a lot has happened since on the topic, including a ground-up rewrite of Dojo’s asynchronous foundations in 1.8+, so it’s about time we take another look at deferreds and promises and how it all fits together in Dojo.

What’s a Promise?

Let’s start with some definitions. A “promise” is a concept to represent the result of an asynchronous operation. If this operation has yet to be completed, the promise is said to be “pending”, and once completed it is said to be “fulfilled”. If the operation was completed successfully the promise is said to be “resolved”. If not, the promise is said to be “rejected”. A promise that is resolved always has a value associated, even if that value is undefined. A rejected promise has a corresponding exception—the reason for its rejection.

Promises, as defined above, have been in Dojo since 1.5, when the Dojo’s Deferred module was extended to support this promise API. Since then promises have become ubiquitous—unless you’re new to the JavaScript language, or you’ve been under a rock for the last several years, it’s likely you’ve seen subject of promises kicked around. They’re becoming part of the language itself, and are beginning to show up in new web platform APIs.

What’s a Deferred?

A “deferred” is a special construct responsible for creating a new “promise”, and determining when this promise should be “resolved” or “rejected”.

Long time Dojo users are sure to be familiar with Dojo’s Deferred class—after all, it’s been around since 0.3 was released in 2008. It’s implementation has evolved over the years, but had become a bit weighed down by the baggage of legacy. With Dojo 1.8 came a clean break with this baggage—a new Deferred module was introduced with a fresh, minimal API. The previous Deferred module is still available at dojo/_base/Deferred, but it’s been deprecated in favor of the dojo/Deferred module and the suite of promise related modules in dojo/promise.

Deferred vs. Promise

Prior to Dojo 1.8 there really wasn’t—a promise was just an abstract concept, an API implemented by Deferred instances. As of Dojo 1.8 promises are no longer abstract. They are real, distinct from deferred, and you’ll find them throughout asynchronous actions with the Dojo codebase. Every time a Deferred is constructed, a corresponding Promise is constructed too. The deferred carries with it the capability to “fulfill” its promise—that is, to “resolve” or “reject” it. This is quite a lot of power, and not the kind of capability you want to give to every caller of your asynchronous function.

On the other hand, the promise instance associated with a Deferred is purely a representation of some future value, and has no power to control what that value should be or when it becomes available. Its capabilities are intentionally restricted to allowing you to inspect and react to its fulfillment, potentially generating new promises representing a different eventual value. As it turns out, this is all the power necessary to consume an asynchronous API.

When to use Deferred?

A deferred contains both a promise and the ability to fulfill that promise. One way to think of it is as a deferred representing work that may or may not be finished, and its promise representing the value (or error) resulting from that work.

There aren’t many circumstances where you’ll need to create and fulfill a Deferred instance—this is only necessary when you have to invoke an asynchronous action that doesn’t already return a promise, e.g. when converting a callback-based API into a promise-based API. Suppose we have a simple echoCallback function, echoing back some provided string with an exclamation point after a brief, random delay using setTimeout:

function echoCallback(value, callback) {
	setTimeout(function() {
		if (typeof value === "string") {
			callback(new Error("Cannot echo " + typeof value));
		} else {
			callback(null, value + "!");
		}
	}, Math.random() * 1000);
}

This is a typical callback scenario using the error-first callback pattern popularized by Node.js. To turn this into a promise-based API, we create a function that invokes this echoCallback function and returns a promise that represents its eventual completion. This function should create a new Deferred instance, invoke the callback to set its fulfilled state, then return the deferred’s promise back to the caller:

require(["dojo/Deferred"], function(Deferred) {
	function echo(value) {
		var deferred = new Deferred();
		// Assume `echoCallback` is already defined
		echoCallback(value, function(error, result) {
			if (error) {
				deferred.reject(error);
			}
			else {
				deferred.resolve(result);
			}
		});
		return deferred.promise;
	}
});

This example, if a bit contrived, is fairly complete—this is just about all there is to turning a callback-style function into one which returns a promise. Dojo’s asynchronous APIs already return promises so you shouldn’t have to write code like this often, but it helps highlight the two different roles played by a deferred and a promise.

It’s important to note that instances of Deferred also implement the promise API, which means they can be used in place of their promise value anywhere a promise is expected. So the code above could use return deferred in place of return deferred.promise, but this is frowned upon as it violates the Principle of Least Authority.

When to promise?

Promises in Dojo are not something you create yourself—they are a natural consequence of calling asynchronous functions in Dojo. Should you ever feel compelled to construct a Dojo Promise what you really want is a Deferred instead. But once you have a reference to a it can be used to generate subsequent promises, either with its then method to tack on subsequent actions, or with the promise utilities introduced in Dojo 1.8.

dojo/when

When you don’t know for sure whether something is a promise or a value you can use the dojo/when module to handle both types uniformly by lifting non-promise values into promises. The when utility is not new—it was introduced back in 1.5 as dojo.when—but it’s worth calling out how important it is for consuming APIs which may or may not return a promise.

dojo/promise/all

The dojo/promise/all is useful when you have multiple promises and you want to wait for all of them to be fulfilled before taking some action. It takes any number promises, as an array or object, and returns a new promise that is fulfilled when all of these promises are resolved successfully, or as soon as one is rejected.

Given an array of promises the all function will resolve to an array of values corresponding to the provided promises:

// Assume our `echo` function above is available as the "app/echo" module
require(["dojo/promise/all", "app/echo"], function(all, echo) {
	all([echo("hello"), echo("there")]).then(function(results) {
		// results: ["hello!", "there!"]
	});
});

You can also provide an object mapping keys to promise values–the all utility will return a new promise that resolves to an object with the keys preserved, and values are the resolved values of each respective promise:

require(["dojo/promise/all", "app/echo"], function(all, echo) {
	all({ alice: echo("hello"), bob: echo("goodbye") })
		.then(function(results) {
		// results: { alice: "hello!", bob: "goodbye!" }
	});
});

The array or object you pass to the all utility can also include non-promise values, so you could think of it like a `whenAll` operation:

require(["dojo/promise/all", "app/echo"], function(all, echo) {
	all({ alice: echo("promise value"), bob: "plain value" })
		.then(function(results) {
		// results: { alice: "promised value!", bob: "plain value!" }
	});
});

dojo/promise/first

If you have multiple promises and you need to know as soon as the first one resolves then dojo/promise/first is the right tool do for the job. It takes multiple promises as an array or object (like dojo/promise/all), and returns a promise that is fulfilled as soon as the first of these promises is fulfilled.

Say we have an array of promises, each representing a type of greeting, and we just need the value fo the first one to resolve, without having to wait on any others:

require([
	"dojo/promise/first", "dojo/_base/array", "app/echo"
], function(all, first, echo) {
	// map `echo` to array of greeting strings to get promises for each
	var greetings = array.map([
		"Hello",
		"Hi",
		"Greetings",
		"Hey up",
		"Whatgwan"
	], echo);
	// as soon as the first greeting promise is fulfilled we can use it
	first(greetings).then(function(greeting) {
		// first available `greeting`, e.g. "Hello!" or "Whatgwan!"
		// selected randomly due to delay added by `echo` function
	});
});

Conclusion

The real power of promises—in Dojo and JavaScript in general—comes from the abstractions they introduce that allows us to model problems closer to the way we think of them. We can compose them together—do this, then that, then all this other stuff—in ways that can make the intent of these actions more explicit. Promises are a simple but powerful tool in the long-running battle to tame the complexity arising from wrangling asynchronicity in everyday code.

We’ve just scratched the surface but we’ve shown a glimpse of how these abstractions can be used, and even what it takes to build them up from scratch. Hopefully this helps to demonstrate some of the patterns and practices made possible by the changes in Dojo 1.8+, and gives you an idea of how this can be applied to your asynchronous problems to make them more legible and maintainable.

Learning more

We cover asynchronous patterns in depth in our Dojo workshops offered throughout the US, Canada, and Europe, or at your location. We also provide expert JavaScript and Dojo support and development services, to help you get the most from JavaScript, Dojo, and managing async efficiently within your application. If you’d like more information about having SitePen assist your organization with JavaScript and Dojo, please contact us to discuss how we can help.

Comments