Get insightful engineering articles delivered directly to your inbox.
By

— 5 minute read

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 throwing and catching 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:

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 };

By
Scott is a Sr. Front End Engineer at InVision.

Like what you've been reading? Join us and help create the next generation of prototyping and collaboration tools for product design teams around the world. Check out our open positions.