Flow Control With Promises

Presented by @trevorburnham

Who’s This Guy?

Stuff We’ll Cover

  • A (Re)-introduction to Promises
  • An overview of Promise libraries
  • Some amazing things Promises can do

Pyramid of Doom


mainWindow.menu("File", function(err, file) {
  if(err) throw err;
  file.openMenu(function(err, menu) {
    if(err) throw err;
    menu.item("Open", function(err, item) {
      if(err) throw err;
      item.click(function(err) {
        if(err) throw err;
        window.createDialog('DOOM!', function(err, dialog) {
          if(err) throw err;
          ...
        });
      });
    });
  });
});
					

Promises Recap (Very Basic)

Instead of doing this…


myLameFunction(myLameCallback);  // ugh, so 1995
					

…you can do this:


myGreatPromise = myAwesomeFunction();
myGreatPromise.then(myTremendousCallback);  // add callbacks later
myGreatPromise.then(myEvenBetterCallback);  // stack ∞ callbacks
myGreatPromise.then(null, myErrorHandler);  // reuse error logic
					

Promises Recap (Also Basic)

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 Recap (Still Pretty Basic)

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

Promises Recap (MOAR ADVANCED!)

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


function RunThreeTasks() {
	function logError(e) {
		console.error(e);
		throw e;  // reject the Promise returned by then
	}
	var task1 = startTask1();
	var task2 = task1.then(startTask2);
	var task3 = task2.then(startTask3);
	var allTasks = task3.then(null, logError);
	return allTasks;
}
					

The Promise returned by ThreeStepTask is fulfilled only when all three tasks have succeeded. If any task fails, logError is called, and the returned Promise is rejected.

Like an Async Throw!

“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

Where can I get some Promises?

Standardize Your Promises

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


standardizedPromise = when(jQueryPromise);
					

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

</recap>

Promises A+ logo

github.com/promises-aplus/

The Contender: Async.js

  • Swiss Army Knife for flow control
  • Expects functions that take (err, results) callback
  • Very popular
Async.js: Nearly 5k stars on GitHub

github.com/caolan/async

Chaining as Flow Control

Remember that chaining example?


function RunThreeTasks() {
	function logError(e) {
		console.error(e);
		throw e;  // reject the Promise returned by then
	}
	var task1 = startTask1();
	var task2 = task1.then(startTask2);
	var task3 = task2.then(startTask3);
	var allTasks = task3.then(null, logError);
	return allTasks;
}
					

This is a waterfall: Each task starts when the last one completed successfully.

Chaining (with Async.js)

Here’s how we’d write that same example using async.waterfall:


function RunThreeTasks(callback) {
	async.waterfall([task1, task2, task3], function(err) {
		if (err) {
			console.error(e);
		};
		callback.apply(null, arguments);
	});
}
					

(Here the task functions take an (err, result) callback rather than returning a Promise.)

Waterfall (with Async.js)

Run an array of async tasks in series, call the callback when all tasks have completed (or one fails):


async.waterfall(tasks, function(err, lastResult) {
	// ...
});
					

Waterfall (with Promises)

What if we want to iterate through an arbitrary array of Promise-returning task functions?


function promiseWaterfall(tasks) {
	finalTaskPromise = tasks.reduce(function(prevTaskPromise, task) {
		return prevTaskPromise.then(task);
	}, resolvedPromise);  // initial value

	return finalTaskPromise;
}
					

Functional idioms FTW!

Parallel (with Async.js)

Run an array of async tasks simultaneously, call the callback with the results when all tasks have succeeded or an error when one has failed:


async.parallel(tasks, function(err, results) {
	// ...
});
					

Parallel (with Promises)

Run an array of async tasks simultaneously, return a Promise that’s fulfilled with the results when all tasks have succeeded or rejected with an error when one has failed:


function promiseParallel(tasks) {
	var results = [];
	taskPromises = tasks.map(function(task) {
		return task();
	});

	return when.all(taskPromises);
}
					

This is such a common idiom that every major Promises implementation comes with an implementation: jQuery calls it when(), Q/when/RSVP call it all().

DIY All


function all(promises) {
  finalPromise = promises.reduce(function(prevPromise, promise, i) {
    return prevPromise.then(function(results) {
      return promise.then(function(result) {
        results.push(result);
        return results;
      })
    });
  }, resolvedPromise);  // a Promise that resolved with []

  return finalPromise;
}
					

Dependency Graphs (with Async.js)

Given an arbitrary graph of identifiers, dependencies, and functions, run those functions in an acceptable order:


// Example adapted from the Async.js docs:
async.auto({
  get_data: getData,
  make_folder: makeFolder,
  write_file: ['get_data', 'make_folder', writeFile],
  email_link: ['write_file', emailLink]
});
					

Can We Do Better?

“So we’ve created a correct optimizing module loader with barely any code, simply by using a graph of lazy promises. We’ve taken the functional programming approach of using value relationships rather than explicit control flow to solve the problem, and it was much easier than if we’d written the control flow ourselves as the main element in the solution.”

Callbacks are Imperative, Promises are Functional,
by @jcoglan

Dependency Graphs (with Lazy Promises)


// Module is defined at https://blog.jcoglan.com/2013/03/30/
new Module('getData', [], getData);
new Module('makeFolder', [], makeFolder);
new Module('writeFile', ['getData', 'makeFolder'], writeFile);
new Module('emailLink', ['writeFile'], emailLink);
					

Thank You!

@trevorburnham