I said before that the magic of promises is that they give us back our precious return and throw. But what does this actually look like in practice?
Every promises gives you a then() method (or catch(), which is just sugar for then(null, ...)). Here we are inside of a then() function:
somePromise().then(function () {
// I'm inside a then() function!
});
What can we do here? There are three things:
- return another promise
- return a synchronous value (or undefined)
- throw a synchronous error
That's it. Once you understand this trick, you understand promises. So let's go through each point one at a time.
This is a common pattern you see in the promise literature, as in the "composing promises" example above:
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
Notice that I'm returning the second promise – that return is crucial. If I didn't say return, then the getUserAccountById() would actually be a side effect, and the next function would receive undefined instead of the userAccount.
Returning undefined is often a mistake, but returning a synchronous value is actually an awesome way to convert synchronous code into promisey code. For instance, let's say we have an in-memory cache of users. We can do:
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
Isn't that awesome? The second function doesn't care whether the userAccount was fetched synchronously or asynchronously, and the first function is free to return either a synchronous or asynchronous value.
Unfortunately, there's the inconvenient fact that non-returning functions in JavaScript technically return undefined, which means it's easy to accidentally introduce side effects when you meant to return something.
For this reason, I make it a personal habit to always return or throw from inside a then() function. I'd recommend you do the same.
Speaking of throw, this is where promises can get even more awesome. Let's say we want to throw a synchronous error in case the user is logged out. It's quite easy:
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
Our catch() will receive a synchronous error if the user is logged out, and it will receive an asynchronous error if any of the promises are rejected. Again, the function doesn't care whether the error it gets is synchronous or asynchronous.
This is especially useful because it can help identify coding errors during development. For instance, if at any point inside of a then() function, we do a JSON.parse(), it might throw a synchronous error if the JSON is invalid. With callbacks, that error would get swallowed, but with promises, we can simply handle it inside our catch() function.