8 Tips for Mastering JavaScript Promises
In the previous post, Understanding JavaScript Promises, we learned how simple and useful Promises can be. Here are 8 tips to help you take advantage of that simplicity, and become a Master of Promises!
1. Know the terminology
Talking about Promises is much easier when you know the right terminology.
- A Promise is pending while it is still working.
- When the work is complete, the Promise is fulfilled (or resolved). The result of the work is called the fulfillment value.
- When an error occurs, the Promise is rejected. The
Error
is called the rejection reason.
2. You don’t need Deferred
!
The Deferred pattern (which includes both deferred
objects and the new Promise
constructor) was designed for wrapping low-level APIs, such as XmlHttpRequest
and setTimeout
. You should rarely need to do this work yourself!
If you’re already using a Promise-returning library, using Deferred like this is a very common anti-pattern:
function getUserProfile(username) {
var defer = Promise.pending();
getUserAsync(username).then(function(user) {
getProfileAsync(user.id).then(function(profile) {
defer.resolve(profile);
}, function(err) {
defer.reject(err);
});
});
return defer.promise;
}
It’s messy, and prone to leaks. Can you spot the leak in the code above?
The alternative is much more powerful:
3. Chain, chain, chain
You’ve already got a Promise. Calling .then
creates a new link in the chain. All you’ve got to do is return the chain!
Look how much better this is:
function getUserProfile(username) {
return getUserAsync(username).then(function(user) {
return getProfileAsync(user.id);
});
}
When you chain, you no longer need to explicitly handle errors – all the “wiring” is automatic.
4. Watch for “runaway Promises”
Whenever you have a Promise, you have to do something with that Promise. If you’re not returning the Promise, then you should be handling the errors. Otherwise, a “runaway Promise” will not be connected to your Promise chain, and will easily cause race conditions or hide errors.
As a rule of thumb, the entry-point (eg. the UI layer, the request handler) should be handling errors, and the rest of your code should be returning the Promises.
function updateUser() {
// By returning the Promise, I don't have to handle errors:
return getUserAsync().then(function(user) {
// Always return nested Promises too!
return getProfileAsync(user).then(function(profile) {
$scope.user = user;
$scope.profile = profile;
});
});
}
button.addEventListener("click", function(ev) {
// This is an "entry-point" of the application,
// and I can't simply "return the Promise" here,
// so I need to handle errors:
showSpinner();
updateUser().then(function() {
hideSpinner();
}).catch(function() {
hideSpinner();
showErrorMessage();
});
});
5. Treat Errors like you’ve always treated Errors
All the traditional rules of throw
ing and catch
ing errors should be applied to Promises as well!
- Only
throw
if you can’t do what you said you can (eg. network error, unexpected data). - Only
catch
if you can do something about it (eg. retry the connection, show an error message). - Don’t “catch and release” – always “catch and rethrow”. If the error handler doesn’t rethrow, the rest of the Promise chain will continue normal execution. This is rarely what you intended!
getUserAsync().then(function(user) {
return user.name;
}).catch(function(err) {
log.error(err);
// Oops, forgot to rethrow!
});
// The above code is the async equivalent of this:
try {
return getUser().name;
} catch (err) {
log.error(err);
// Oops, forgot to rethrow!
// Continue normal execution:
}
6. Learn your library
In theory, a Promise only requires a .then
method. If it is “thenable”, it’ll interop with any other Promise library. However, every Promise library adds its own host of additional features for handling everyday tasks. Learn your library, and you’ll unlock the power of your Promise’s potential!
Here are some of the most popular Promise implementations:
- Bluebird - very popular for NodeJS applications
- Q which inspired Angular’s
$q
service - jQuery’s Promise
- Native ES6 Promises, or the ES6 Promise polyfill
7. NodeJS developers can promisify
all the things!
The majority of NodeJS modules use an async pattern called error-first callbacks instead of Promises. Fortunately, there is an effortless way to convert any error-first-callback module into a Promise-returning module! An incredible utility, called promisifyAll, does this for you, and it couldn’t be easier to use:
var Promise = require("bluebird");
var fs = Promise.promisifyAll(require("fs"));
This will give you the original fs
module, unharmed. It still has its original methods, like fs.readFile(filename, callback)
and fs.writeFile(filename, data, callback)
. However, the fs
module has now been augmented with Promise-returning methods, called readFileAsync(filename) => Promise
and writeFileAsync(filename, data) => Promise
and so on!
// Now, I can use `readFileAsync` instead of `readFile`
fs.readFileAsync("data.txt").then(function(data) {
console.log(data);
});
promisifyAll
cleverly traverses the whole module, too, and adds *Async
methods onto nested modules and class prototypes!
8. Look into the Future: Async + Await
Looking into the future will change the way you see today’s Promises!
Here’s a sample of an upcoming feature: an async
function:
async function getUserInfo(username, password) {
var userId = await getUserId(username, password);
var profile = await getProfile(userId);
var projects = await getProjects(userId);
return { userId, profile, projects };
}
The async
+ await
syntax makes Promises an implementation detail, that you no longer need to think about! Technically, the getUserInfo
function returns a Promise, as do getUserId
, getProfile
, and getProjects
. However, the await
keyword waits for the promise, and then gives you the value. It’s the same as calling .then
to get the results, but this completely eliminates the callbacks and the nesting! This code looks almost identical to synchronous code.
If you’re not lucky enough to be bleeding-edge, or transpiling with something like Babel, you’ll have to stick with the Promise equivalent:
// Same code, using Promises directly
function getUserInfo(username, password) {
return getUserId(username, password).then(function(userId) {
return getProfile(userId).then(function(profile) {
return getProjects(userId).then(function(projects) {
return { userId, profile, projects };
});
});
});
}
So when you look at Promise code like this, you should realize that 3 of these return
statements are actually just await
statements in disguise. Realize that this function only has one real return point: the final return { userId, profile, projects };