JavaScript Promise API

While synchronous code is easier to follow and debug, async is generally better for performance and flexibility. Why hold “up the show” when you can trigger numerous requests at once and then handle them when each is ready? Promises are becoming a big part of the JavaScript world, with many new APIs being implemented with the promise philosophy. Let’s take a look at promises, the API, how it’s used!

Promises in the Wild

The XMLHttpRequest API is async but does not use the Promises API. There are a few native APIs that now use promises, however:

Promises will only become more prevalent so it’s important that all front-end developers get used to them. It’s also worth noting that Node.js is another platform for Promises (obviously, as Promise is a core language feature).

Testing promises is probably easier than you think because setTimeout can be used as your async “task”!

Basic Promise Usage

A promise is created with the new keyword and the promise provides resolve and reject functions to the provided callback:

var p = new Promise(function(resolve, reject) {
	
	// Do some processing

	if(/* good condition */) {
		resolve('Success!');
	}
	else {
		reject('Failure!');
	}
});

It’s up to the developer to manually call resolve or reject within the body of the callback based on the result of their given task. Also note that you don’t need to complete async tasks within the promise — if it’s possible that an async action will be taken, using a promise will be best so that you can always count on a promise coming out of a given function. For example:

var userCache = {};

// There's a chance an async action may take place
// i.e. if the user isn't in cache, use fetch to get their info
// This is a perfect function to return a promise from
function getUserDetail(username) {
	return new Promise(function(resolve, reject) {
		// Not async but since we're in a promise we can resolve ourself
		if(userCache[username]) {
			resolve(userCache[username]);
		}
		else {
			// Use the fetch API to get the information
			fetch('users/' + username + '.json')
				.then(function(result) {
					userCache[username] = result;
					resolve(result);
				})
				.catch(function() {
					reject(Error('Could not find user: ' + username));
				});
		}
	});
}

// Use the promise 
getUserDetail('davidwalsh')
.then(function(userInfo) {
	// Do something with the user info
})
.catch(function(err) {
	// Do something with the error
});

Since a promise is always returned, you can always use the then and catch methods on its return value!

then

All promise instances get a then method which allows you to react to the promise. The first then method callback receives the result given to it by the resolve() call:

new Promise(function(resolve, reject) {
	// A mock async action using setTimeout
	setTimeout(function() { resolve(10); }, 3000);
})
.then(function(result) {
	console.log(result);
});

// From the console:
// 10

The then callback is triggered when the promise is resolved. You can also chain then method callbacks:

new Promise(function(resolve, reject) { 
	// A mock async action using setTimeout
	setTimeout(function() { resolve(10); }, 3000);
})
.then(function(num) { console.log('first then: ', num); return num * 2; })
.then(function(num) { console.log('second then: ', num); return num * 2; })
.then(function(num) { console.log('last then: ', num);});

// From the console:
// first then:  10
// second then:  20
// last then:  40

Each then receives the result of the previous then‘s return value.

catch

The catch callback is executed when the promise is rejected:

new Promise(function(resolve, reject) {
	// A mock async action using setTimeout
	setTimeout(function() { reject('Done!'); }, 3000);
})
.then(function(e) { console.log('done', e); })
.catch(function(e) { console.log('catch: ', e); });

// From the console:
// 'catch: Done!'

What you provide to the reject method is up to you. A frequent pattern is sending an Error to the catch:

reject(Error('Data could not be found'));

Promise.all

Think about JavaScript loaders: there are times when you trigger multiple async interactions but only want to respond when all of them are completed — that’s where Promise.all comes in. The Promise.all method takes an array of promises and fires one callback once they are all resolved:

Promise.all([promise1, promise2]).then(function(results) {
	// Both promises resolved
})
.catch(function(error) {
	// One or more promises was rejected
});

An perfect way of thinking about Promise.all is firing off multiple AJAX (via fetch) requests at one time:

var request1 = fetch('/users.json');
var request2 = fetch('/articles.json');

Promise.all([request1, request2]).then(function(results) {
	// Both promises done!
});

You could combine APIs like fetch and the Battery API since they both return promises:

Promise.all([fetch('/users.json'), navigator.getBattery()]).then(function(results) {
	// Both promises done!
});

Dealing with rejection is, of course, hard. If any promise is rejected the catch fires for the first rejection:

var req1 = new Promise(function(resolve, reject) { 
	// A mock async action using setTimeout
	setTimeout(function() { resolve('First!'); }, 4000);
});
var req2 = new Promise(function(resolve, reject) { 
	// A mock async action using setTimeout
	setTimeout(function() { reject('Second!'); }, 3000);
});
Promise.all([req1, req2]).then(function(results) {
	console.log('Then: ', one);
}).catch(function(err) {
	console.log('Catch: ', err);
});

// From the console:
// Catch: Second!

Promise.all will be super useful as more APIs move toward promises.

Promise.race

Promise.race is an interesting function — instead of waiting for all promises to be resolved or rejected, Promise.race triggers as soon as any promise in the array is resolved or rejected:

var req1 = new Promise(function(resolve, reject) { 
	setTimeout(function() { resolve('First!'); }, 8000);
});
var req2 = new Promise(function(resolve, reject) { 
	setTimeout(function() { resolve('Second!'); }, 3000);
});
Promise.race([req1, req2]).then(function(one) {
	console.log('Then: ', one);
}).catch(function(one, two) {
	console.log('Catch: ', one);
});

// From the console:
// Then: Second!

A use case could be triggering a request to a primary source and a secondary source (in case the primary or secondary are unavailable).

Get Used to Promises

Promises have been a hot topic for the past few years (or the last 10 years if you were a Dojo Toolkit user) and they’ve gone from a JavaScript framework pattern to a language staple. It’s probably wise to assume you’ll be seeing most new JavaScript APIs being implemented with a promise-based pattern…

…and that’s a great thing! Developers are able to avoid callback hell and async interactions can be passed around like any other variable. Promises take some time getting used to be the tools are (natively) there and now is the time to learn them!

Leave a Reply

Your email address will not be published. Required fields are marked *