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!
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.
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. 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. 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. 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. 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. 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. 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. 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. 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. 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. var result;
try {
result = process(getInput());
} catch (ex) {
result = handleError(ex);
}
var resultPromise =
getInputPromise()
.then(processAsync)
.then(undefined, handleErrorAsync);
The sync ⇔ async parallel
16. var result;
try {
result = process(getInput());
} catch (ex) {
result = handleError(ex);
}
var resultPromise =
getInputPromise()
.then(processAsync)
.catch(handleErrorAsync);
The sync ⇔ async parallel
17. Case 1: simple functional transform
var user = getUser();
var userName = user.name;
// becomes
var userNamePromise = getUser().then(function (user) {
return user.name;
});
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. 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. 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. 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. 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.
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. 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. 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);
}
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. 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. 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. 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. 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. 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.