The Promised Land (in Angular)

22,147 views

Published on

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!

Published in: Technology, Education, Business
1 Comment
39 Likes
Statistics
Notes
  • Great points about advantages of promises! However, I'd like to note that some of the recommendations at the end are outdated with regards to AngularJS 1.2. For example, $q now supports 'finally' instead of 'always' to be closer aligned to Q. Most importantly, the async behavior has been fixed to always fire, even if there is no upcoming digest cycle.

    From Igor Minar:
    https://github.com/angular/angular.js/issues/2881#issuecomment-39017671

    'In any case, the Angular-specfic slides in the slide deck you are referring to are outdated or plain wrong. Unfortunately Domenic misunderstood the reasoning behind some of the $q design decisions and didn't check with us before publishing the slides.'
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total views
22,147
On SlideShare
0
From Embeds
0
Number of Embeds
444
Actions
Shares
0
Downloads
197
Comments
1
Likes
39
Embeds 0
No embeds

No notes for slide
  • First benefit: separating outputs from inputs.
  • First benefit: separating outputs from inputs.
  • - Consistency gains: never call back with more than one fulfillment value or rejection reason.
  • Just like a series of imperative statements.

    NB: “resolved” does not mean what many people think it means
  • Pop quiz: what would happen if I consolidated the last two lines?
  • Angular has $q.all(); the others are more just to illustrate.
  • This is actually a cool example because it will work with any Promises/A+ library.
  • The Promised Land (in Angular)

    1. 1. The Promised Land by @domenic in
    2. 2. 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
    3. 3. 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.
    4. 4. Promises, in General
    5. 5. 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
    6. 6. Async with callbacks // Ask for permission to show notifications Notification.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...");
    7. 7. Async with events var 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");
    8. 8. Async with WTFBBQ var 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();
    9. 9. 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.
    10. 10. 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
    11. 11. 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
    12. 12. 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)
    13. 13. 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
    14. 14. 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
    15. 15. var result; try { result = process(getInput()); } catch (ex) { result = handleError(ex); } var resultPromise = getInputPromise() .then(processAsync) .then(undefined, handleErrorAsync); The sync ⇔ async parallel
    16. 16. var result; try { result = process(getInput()); } catch (ex) { result = handleError(ex); } var resultPromise = getInputPromise() .then(processAsync) .catch(handleErrorAsync); The sync ⇔ async parallel
    17. 17. Case 1: simple functional transform var user = getUser(); var userName = user.name; // becomes var userNamePromise = getUser().then(function (user) { return user.name; });
    18. 18. 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; });
    19. 19. 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); });
    20. 20. 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); });
    21. 21. 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
    22. 22. 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.
    23. 23. Always async function getUser(userName, onSuccess, onError) { if (cache.has(userName)) { onSuccess(cache.get(userName)); } else { $.ajax("/user?" + userName, { success: onSuccess, error: onError }); } }
    24. 24. Always async console.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName); }); console.log("2"); // 1, 2, Domenic
    25. 25. Always async console.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName); }); console.log("2"); // 1, Domenic, 2
    26. 26. Always async function getUser(userName) { if (cache.has(userName)) { return $q.when(cache.get(userName)); } else { return $http.get("/user?" + userName); } }
    27. 27. Always async console.log("1"); getUser("ddenicola“).then(function (user) { console.log(user.firstName); }); console.log("2"); // 1, 2, Domenic (every time!)
    28. 28. getUser("Domenic", function (user) { getBestFriend(user, function (friend) { ui.showBestFriend(friend); }); }); Async “exception propagation”
    29. 29. 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”
    30. 30. getUser("Domenic") .then(getBestFriend) .then(ui.showBestFriend) .catch(ui.error); Async “exception propagation”
    31. 31. 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
    32. 32. Building promise abstractions function 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); }
    33. 33. Building promise abstractions function 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); }
    34. 34. Promises, in Angular
    35. 35. The digest cycle
    36. 36. 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!
    37. 37. 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
    38. 38. 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
    39. 39. 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
    40. 40. 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 });
    41. 41. 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.
    42. 42. Thanks! promisesaplus.com @promisesaplus

    ×