the promised land (in angular)
DESCRIPTION
Promises are a popular pattern for asynchronous operations in JavaScript, existing in some form in every client-side framework in widespread use today. We'll give a conceptual and practical intro to promises in general, before moving on to talking about how they fit into Angular. If you've ever wondered what exactly $q was about, this is the place to learn!TRANSCRIPT
ThePromisedLand
by @domenic
in
http://domenic.me
https://github.com/domenic
https://npmjs.org/~domenic
http://slideshare.net/domenicdenicola
THINGS I’M DOING The Promises/A+ and ES6 promise specs Working on Q The Extensible Web Manifesto
Domenic Denicola
Angular is enlightened• Like most other client-side frameworks these days, Angular uses
promises for everything async:• $timeout• $http + response interceptors• $resource• $routeProvider.when
• Its built-in promise library, $q, is pretty good.
• But first, let’s take a step back and start from the beginning.
Promises, in General
Web programming is async• I/O (like XMLHttpRequest, IndexedDB, or waiting for the user to
click) takes time
• We have only a single thread
• We don’t want to freeze the tab while we do I/O
• So we tell the browser:• Go do your I/O• When you’re done, run this code• In the meantime, let’s move on to some other code
Async with callbacks// Ask for permission to show notificationsNotification.requestPermission(function (result) { // When the user clicks yes or no, this code runs. if (result === 'denied') { console.log('user clicked no'); } else { console.log('permission granted!'); }}); // But in the meantime, this code continues to run.console.log("Waiting for the user...");
Async with eventsvar request = indexedDB.open("myDatabase"); request.onsuccess = function () { console.log('opened!');};request.onerror = function () { console.log('failed');}; console.log("This code runs before either of those");
Async with WTFBBQvar xhr = new XMLHttpRequest();xhr.onreadystatechange = function () { if (this.readyState === this.DONE) { if (this.status === 200) { console.log("got the data!" + this.responseText); } else { console.log("an error happened!"); } }}; xhr.open("GET", "somefile.json");xhr.send();
These APIs are a hack• They are literally the simplest thing that could work.
• But as a replacement for synchronous control flow, they suck.
• There’s no consistency.
• There’s no guarantees.
• We lose the flow of our code writing callbacks that tie together other callbacks.
• We lose the stack-unwinding semantics of exceptions, forcing us to handle errors explicitly at every step.
Instead of calling a passed callback, return a promise:
var promiseForTemplate = $http.get("template.html");
promiseForTemplate.then( function (template) { // use template }, function (err) { // couldn’t get the template });
Promises are the right abstraction
function getPromiseFor5() { var d = $q.defer(); d.resolve(5); return d.promise;} getPromiseFor5().then(function (v) { console.log('this will be 5: ' + v);});
Creating a promise
function getPromiseFor5After1Second() { var d = $q.defer(); setTimeout(function () { d.resolve(5); }, 1000); return d.promise;} getPromiseFor5After1Second().then(function (v) { // this code only gets run after one second console.log('this will be 5: ' + v);});
Creating a promise (more advanced)
promiseForResult.then(onFulfilled, onRejected);
• Only one of onFulfilled or onRejected will be called.
• onFulfilled will be called with a single fulfillment value (⇔ return value).
• onRejected will be called with a single rejection reason (⇔ thrown exception).
• If the promise is already settled, the handlers will still be called once you attach them.
• The handlers will always be called asynchronously.
Promise guarantees
var transformedPromise = originalPromise.then(onFulfilled, onRejected);
• If the called handler returns a value, transformedPromise will be resolved with that value:• If the returned value is a promise, we adopt its state.
• Otherwise, transformedPromise is fulfilled with that value.
• If the called handler throws an exception, transformedPromise will be rejected with that exception.
Promises can be chained
var result; try { result = process(getInput());} catch (ex) { result = handleError(ex);}
var resultPromise = getInputPromise() .then(processAsync) .then(undefined, handleErrorAsync);
The sync ⇔ async parallel
var result; try { result = process(getInput());} catch (ex) { result = handleError(ex);}
var resultPromise = getInputPromise() .then(processAsync) .catch(handleErrorAsync);
The sync ⇔ async parallel
Case 1: simple functional transform var user = getUser(); var userName = user.name; // becomes var userNamePromise = getUser().then(function (user) { return user.name; });
Case 2: reacting with an exception var user = getUser(); if (user === null) throw new Error("null user!"); // becomes var userPromise = getUser().then(function (user) { if (user === null) throw new Error("null user!"); return user; });
Case 3: handling an exception try { updateUser(data); } catch (ex) { console.log("There was an error:", ex); } // becomes var updatePromise = updateUser(data).catch(function (ex) { console.log("There was an error:", ex); });
Case 4: rethrowing an exception try { updateUser(data); } catch (ex) { throw new Error("Updating user failed. Details: " + ex.message); } // becomes var updatePromise = updateUser(data).catch(function (ex) { throw new Error("Updating user failed. Details: " + ex.message); });
var name = promptForNewUserName(userId); updateUser({ id: userId, name: name }); refreshUI(); // becomes promptForNewUserName(userId) .then(function (name) { return updateUser({ id: userId, name: name }); }) .then(refreshUI);
Bonus async case: waiting
Key features In practice, here are some key capabilities promises give you:
• They are guaranteed to always be async.
• They provide an asynchronous analog of exception propagation.
• Because they are first-class objects, you can combine them easily and powerfully.
• They allow easy creation of reusable abstractions.
Always asyncfunction getUser(userName, onSuccess, onError) { if (cache.has(userName)) { onSuccess(cache.get(userName)); } else { $.ajax("/user?" + userName, { success: onSuccess, error: onError }); }}
Always asyncconsole.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName);}); console.log("2");
// 1, 2, Domenic
Always asyncconsole.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName);}); console.log("2");
// 1, Domenic, 2
Always asyncfunction getUser(userName) { if (cache.has(userName)) { return $q.when(cache.get(userName)); } else { return $http.get("/user?" + userName); }}
Always asyncconsole.log("1"); getUser("ddenicola“).then(function (user) { console.log(user.firstName);}); console.log("2");// 1, 2, Domenic (every time!)
getUser("Domenic", function (user) { getBestFriend(user, function (friend) { ui.showBestFriend(friend); });});
Async “exception propagation”
getUser("Domenic", function (err, user) { if (err) { ui.error(err); } else { getBestFriend(user, function (err, friend) { if (err) { ui.error(err); } else { ui.showBestFriend(friend, function (err, friend) { if (err) { ui.error(err); } }); } }); }});
Async “exception propagation”
getUser("Domenic") .then(getBestFriend) .then(ui.showBestFriend) .catch(ui.error);
Async “exception propagation”
Because promises are first-class objects, you can build simple operations on them instead of tying callbacks together:
// Fulfills with an array of results when both fulfill, or rejects if either reject all([getUserData(), getCompanyData()]); // Fulfills with single result as soon as either fulfills, or rejects if both reject any([storeDataOnServer1(), storeDataOnServer2()]); // If writeFile accepts promises as arguments, and readFile returns one: writeFile("dest.txt", readFile("source.txt"));
Promises as first-class objects
Building promise abstractionsfunction timer(promise, ms) { var deferred = $q.defer(); promise.then(deferred.resolve, deferred.reject); setTimeout(function () { deferred.reject(new Error("oops timed out")); }, ms); return deferred.promise;} function httpGetWithTimer(url, ms) { return timer($http.get(url), ms);}
Building promise abstractionsfunction retry(operation, maxTimes) { return operation().catch(function (reason) { if (maxTimes === 0) { throw reason; } return retry(operation, maxTimes - 1); });} function httpGetWithRetry(url, maxTimes) { return retry(function () { return $http.get(url); }, maxTimes);}
Promises, in Angular
The digest cycle
function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (this.readyState === this.DONE && this.status === 200) { $scope.text = this.responseText; } }; xhr.open("GET", "somefile.json"); xhr.send(); };} // Doesn’t work, because the callback function is outside the digest cycle!
function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { jQuery.get("somefile.json").then(function (responseText) { $scope.text = responseText; }); };}
// Still doesn't work: same problem
function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { jQuery.get("somefile.json").then(function (responseText) { $scope.apply(function () { $scope.text = responseText; }); }); };}
// Works, but WTF
function MyController($scope, $http) { $scope.text = "loading"; $scope.doThing = function () { $http.get("somefile.json").then(function (response) { $scope.text = response.data; }); };}
// Works! Angular’s promises are integrated into the digest cycle
Useful things• $q.all([promise1, promise2, promise3]).then(function (threeElements) { … });
• $q.all({ a: promiseA, b: promise }).then(function (twoProperties) { … });
• Progress callbacks:• deferred.notify(value)• promise.then(undefined, undefined, onProgress)• But, use sparingly, and be careful
• $q.when(otherThenable), e.g. for jQuery “promises”
• promise.finally(function () { // happens on either success or failure});
Gotchas• Issue 7992: catching thrown errors causes them to be logged anyway
• Writing reusable libraries that vend $q promises is hard• $q is coupled to Angular’s dependency injection framework• You have to create an Angular module, which has limited audience
• Angular promises are not as full-featured as other libraries:• Check out Q or Bluebird• But to get the digest-cycle magic, you need
qPromise.finally($scope.apply).
• Deferreds are kind of lame compared to the ES6 Promise constructor.
• Progress callbacks are problematic.
Thanks!
promisesaplus.com @promisesaplus