JavaScript Promises:
A Sane Approach to Async Code

Presented by @trevorburnham
at Mobile+Web DevConf 2013

Who’s This Guy?

  • 6'1", 150lbs
  • Aries
  • INTJ
  • Book author
  • JS dev at HubSpot

Book #1

CoffeeScript book

CoffeeScript: Accelerated JavaScript Development

Book #2

Async JavaScript book

Async JavaScript: Build More Responsive Apps with Less Code

Book #3

npm book

The npm Book: The Essential Guide to the Node Package Manager

We're Hiring!

HubSpot

Refer a friend, make $30,000

Stuff We’ll Cover

  • Why do you need Promises?
  • How do you use Promises?
  • How did you ever live without Promises?

The Trouble with Callbacks: Part 1


// Send score to server, give callback the rankings it returns
function submitPlayerScore(score, onSuccess, onError) {
	post('/score', score, {
		success: onSuccess,
		error: onError
	});
}
						

The Trouble with Callbacks: Part 2


// Send score to server, show score animation, invoke callback
// with rankings when the animation and network call both complete
function submitPlayerScore(score, onSuccess, onError) {
	var animationCompleted, rankings;
	post('/score', score, {
		success: function(data) {
			rankings = data;
			if (animationCompleted) onSuccess(rankings);
		},
		error: onError
	});
	startScoreAnimation(function() {
		animationCompleted = true;
		if (rankings) onSuccess(rankings);
	});
}
						

The Trouble with Callbacks: Part 3


// Get auth token, send score and token to server, show score
// animation, invoke callback with rankings when that's all done
function submitPlayerScore(score, onSuccess, onError) {
	var animationCompleted, rankings;
	get('/token', {
		success: function(token) {
			post('/score', {token: token, score: score}, {
				success: function(data) {
					rankings = data;
					if (animationCompleted) onSuccess(rankings);
				},
				error: onError
			});
		},
		error: onError
	});
	startScoreAnimation(function() {
		animationCompleted = true;
		if (rankings) onSuccess(rankings);
	});
}
						

Meanwhile, in a multi-threaded language...


function submitPlayerScore(score) {
	scoreAnimation.start();
	var token = get('/token');
	var data = {token: token, score: score};
	var rankings = post(data, '/score');

	// Wait for animation to complete
	while (!scoreAnimation.isComplete) {
		Thread.sleep(0);
	}
	return rankings;
}
						

But Threads Have Their Own Problems

The trouble with threads

Threads? Where we're going,
we don't need "threads"

Promises logo

https://github.com/promises-aplus/promises-spec

Promises in the Abstract: Part 1

Instead of doing this…


myLameFunction(myCallback);  // ugh, so 1995
						

…you can do this…


myGreatPromise = myAwesomeFunction();
						

…and then all of this:


myGreatPromise.then(myCallback); 	    // add callbacks later
myGreatPromise.then(myExtraCallback);  	    // stack ∞ callbacks
myGreatPromise.then(null, myErrorHandler);  // reuse error logic
							

Promises in the Abstract: Part 2

A Promise represents a task (usually async) that can either succeed or fail. In Promise-ese, we say that the task was either fulfilled* or rejected.


aBeautifulPromise.then(
	function() { /* This runs if the Promise is fulfilled... */ },
	function() { /* ...and this runs if the Promise is rejected */ }
);
						

*In jQuery, Promises are said to be resolved on success. But the Promises/A+ spec uses the word “resolved” to mean “either fulfilled or rejected,” and Promises/A+ is awesome.

Promises in the Abstract: Part 3

  • A Promise that's neither fulfilled nor rejected is pending.
  • A Promise's state can only change in two ways:
    • pendingfulfilled
    • pendingrejected
  • In short, a Promise can only be fulfilled or rejected once.
  • Just as a function can only return or throw an exception!

The Tao of Promises

“The thing is, promises are not about callback aggregation. That’s a simple utility. Promises are about something much deeper, namely providing a direct correspondence between synchronous functions and asynchronous functions.”

You’re Missing the Point of Promises,
by @domenic

Chaining Promises

The real magic of Promises is that you can chain them to represent a series of async tasks:


// Perform three async tasks sequentially
function RunThreeTasks() {
	var task1 = startTask1();
	var task2 = task1.then(startTask2);
	var task3 = task2.then(startTask3);
	return task3;
}
						

The Promise returned by RunThreeTasks is fulfilled only when all three tasks have succeeded, and rejected if any of the tasks fails.

Chaining: How Does it Work?

https://github.com/promises-aplus/promises-spec


var after = before.then(onFulfilled, onRejected);
						

after starts in a pending state. If before is fulfilled:

  1. If onFulfilled is not a function, after is fulfilled with the same value as before.
  2. If onFulfilled returns a value (other than a Promise), after is fulfilled with that value.
  3. If onFulfilled throws an exception, after is rejected with that exception.
  4. If onFulfilled returns a Promise, after will do whatever that Promise does!

Chaining: Error Handling

The same chaining rules apply to the onRejected handler, which means that errors cascade to the next Promise:


// Perform three async tasks sequentially, log any error
function RunThreeTasks() {
	var task1 = startTask1();
	var task2 = task1.then(startTask2);
	var task3 = task2.then(startTask3);
	task3.then(null, logError);
	return task3;
}
						

Wow! Where can I get some Promises?

Make Your Own Promises

To make a Promise that you can fulfill or reject yourself, you typically create a Deferred object that contains both a Promise and methods that can resolve it.

Here's an example using Q.js:


function waitJustOneSecond() {
	var deferred = Q.defer();
	setTimeout(function() {
		deferred.resolve('Thanks for your patience!');
	}, 1e3);
	return deferred.promise;
}
						

What if I'm already using callbacks?

Turning a callback function into a Promise-returning function is easy!


// Invoke callback the next time the user taps the screen
function onNextTap(callback) {
	...
}
						

// Fulfill promise the next time the user taps the screen
function promisifiedOnNextTap() {
	var deferred = Q.defer();
	onNextTap(deferred.resolve);
	return deferred.promise;
}
						

Promises in Parallel

Every major Promises library comes with a function that takes a list of Promises and returns a Promise with these properties:

  1. If all Promises that were passed are fulfilled, the returned Promise is fulfilled with those values.
  2. If any of the Promises that were passed is rejected, the returned Promise is immediately rejected with that error.

jQuery calls this function when().
Q/when/RSVP call it all().

Callback Trouble? Not anymore!


// Get auth token, send score and token to server, show score
// animation, invoke callback with rankings when that's all done
function submitPlayerScore(score) {
	var getTokenPromise = get('/token');
	var submitScorePromise = getTokenPromise.then(function(token) {
		return post('/score', {token: token, score: score});
	});
	var scoreAnimationPromise = startScoreAnimation();

	return Q.all([submitScorePromise, scoreAnimationPromise]);
}
						

Standardize Your Promises

jQuery doesn't recognize Promises/A+ Promises, but all of the major Promises/A+ libraries recognize jQuery Promises:


					standardizedPromise = Q.when(jQueryPromise);
						

You can even monkey-patch jQuery (overriding $.Deferred) to make it return standards-compliant Promises: http://jsfiddle.net/jdiamond/ZSpJX/

What Have We Learned?

  • Promises make async code fun!!
  • Write your async functions to return Promises instead of (or in addition to!) a callback.
  • There are lots of libraries for making Promises.
  • (But please use Promises/A+.)

One last thing...

Promises are coming to the DOM!*

http://dom.spec.whatwg.org/#futures

*Not actually in any browsers yet...

Thank You!
Have fun!!

@trevorburnham

Slides: http://trevorburnham.com/presentations/sane-async-with-promises