This is part 2 in a series of posts that will help familiarize you with many of the features of the recently-approved ECMAScript 6 specification (aka ES2015, ES Harmony, ESNext) as well as go through some of the proposed changes for ES6+/ES7. It focuses on providing realistic examples of how your code changes before and after applying the new ES6 and beyond features.
Other posts in the series:
Part 2 - Promises, generators, and async
This post will focus on different methods of control flow that are available in ES6 and ES7. We’ll take a look at Promises, which are commonly used today, and see how they can be combined with some of the newer features to create some truly clean code.
Promises
Promises provide a way to better manage the flow of asynchronous events by treating those events as a first class object that can have multiple completion or error handlers chained together. Instead of requiring a callback function as an argument, functions that utilize promises can return synchronously an object that represents a “future value” of whatever is being computed.
Promises are not a new concept in JavaScript – there are many competing libraries that implement the promises API (Bluebird, RSVP, Q, and many, many, more).
However, what’s new in ES6 is that promises are now native! There is no more need to have to consume a third party library (or have a dependency on third party libraries that use multiple different Promise implementations).
The basic syntax of a Promise looks like: new Promise(function (resolve, reject) {})
, where the resolve function is called when the asynchronous action succeeds, and the reject function is called if the action fails or has an error.
Promises vs Callbacks
One of the major ways that promises can change your code is by reducing the need for long stacks of nested callbacks. Consider a situation where you need to call a REST api that fetches a user by a user id and then says hello to user the if they are “friendly”.
Before
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// callback is a Node.JS style callback where the first parameter is the error (if any), and the 2nd parameter is the success object.
function getUser(userId, callback) {
$.getJSON(`/user/${userId}`, function (user) {
if (user) {
callback(null, user);
} else {
callback(new Error('User does not exist'));
}
});
}
function sayHelloToUser(userId, callback) {
getUser(userId, function (err, user) {
if (err) {
callback(err);
} else {
if (user.isFriendly) {
console.log(`Hello ${user.name}!`);
callback(null, user);
} else {
callback(new Error('User is not friendly'));
}
}
});
}
var userId = 1;
sayHelloToUser(userId, function (err, user) {
if (err) {
console.error(`Failed not say hello to user id: ${userId}`, err);
}
});
As you can see here, it is becoming very messy to deal with propagating values and especially errors throughout the callback chain. Furthermore, if a synchronous error (via throw) were to occur, it would be silently swallowed and no more code would be executed afterwards.
After
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function getUser(userId) {
return new Promise(function (resolve, reject) {
$.getJSON(`/user/${userId}`, function (user) {
if (user) {
resolve(user);
} else {
reject(new Error('User does not exist'));
}
});
});
}
function sayHelloToUser(userId, callback) {
return getUser(userId).then(function (user) {
if (user.isFriendly) {
console.log(`Hello ${user.name}!`);
} else {
throw new Error('User is not friendly');
}
});
}
var userId = 1;
sayHelloToUser().catch(function (err) {
console.error(`Failed to say hello to user id: ${userId}`, err);
});
We have slightly more code to wrap the callback-based code into a Promise, but after we have a Promise we start to see how it can help reduce some complexity. Notice that when we create the promise we pass resolve and reject functions that are called based on whether the user exists or not.
The real gains in this example come when we deal with triggering and propagating errors.
Rejected promises are passed to any associated .catch handlers, and if there is an unhandled rejected promise (you forgot to .catch) it will raise an unhandled rejection event (which is automatically logged in the browser) – no more silently swallowed errors!
Synchronous errors (manually thrown, undefined reference errors, etc…) are automatically converted to a rejected promise and passed to any .catch handlers – so you can start using throw again just like in normal synchronous code.
Promise functions
Promises also have some static functions that act as shorthand or make working with multiple Promises easier, and I’ll go over a few here that I find the most useful.
To see the full list of functions, visit the MDN page.
Promise.resolve
Promise.resolve is a function that takes a value (any value) and returns a Promise that is already in the “resolved” state. This can be useful if you want to write a mock service that acts as if it were calling out asynchronously but instead just resolves with some static data.
However, if you pass a Promise to Promise.resolve it has some special semantics: it will return a new Promise that is resolved when the original promise is resolved, or rejected if the original promise is rejected. This is helpful if you have one path of a function that might complete synchronously but another path that calls an asynchronous function – you can just call Promise.resolve on either of them and it will work as you expected.
One common example of this situation is when you check a local cache and want to return right away on a hit, but need to call out to an asynchronous service if it misses.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var usersCache = {}; // key'd by userId
// returns a promise that when resolved, contains the user's information
function getUser(userId) {
var user = usersCache[userId];
if (!user) {
user = fetchAsync(`/users/${userId}`); // returns a Promise
}
return Promise.resolve(user);
}
getUser(1).
then(function (user) {
console.log('I found my user!');
});
Regardless of whether it came from cache or from a fetchAsync, the calling code doesn’t have to know any different.
Promise.reject
Promise.reject is shorthand for creating a Promise in an already-rejected state by passing it a rejection reason. This could be useful for doing synchronous validations before an asynchronous call and treating it like it is all asynchronous.
1
2
3
4
5
6
7
8
9
10
11
12
function getUser(userId) {
if (userId < 0) {
return Promise.reject(new Error('User id must be > 0'));
}
return fetchAsync(`/users/${userId}`);
}
getUser(-1)
.catch(function (error) {
console.error(error);
});
Promise.all
Sometimes you have to call multiple asynchronous functions in a row and wait for all of their results before continuing - Promise.all is a simple way to do that.
It takes an array of Promises and returns a single promise that is resolved when all of those Promises are resolved. The result of the resolution is the result of each individual Promise, in an array that is ordered the same way as the original Promises.
If any of the Promises in the array are rejected, the returned Promise is immediately rejected as well.
1
2
3
4
5
6
7
8
9
10
11
12
13
function getUser(userId){
return fetchAsync(`/users/${userId}`);
}
function getUsers(userIds) {
var promises = userIds.map(getUser); // calls the getUser function on each id and returns an array of Promises
return Promise.all(promises);
}
getUsers([1,2])
.then(function (users) {
console.log('got some users', users); // [ { userId: 1 }, { userId: 2} ]
});
Generators, yield, and function *
Generator functions (function *) are a new type of function standardized in ES6 that enable you to pause the execution of a function and return values to calling code which can then resume the execution of the original function at a later time.
This is a bit of a departure from “normal” JavaScript where once your function starts execution it continues to completion.
When you call a generator function it will return a Generator object to the calling function and immediately suspend execution of the generator function.
Inside of the generator function, you use the yield
keyword to return a value.
After you have a Generator, you can retrieve the value that was yielded by the generator function by calling generator.next()
and checking the ‘value’ property. When your generator function yields no more values after calling generator.next(), it sets the ‘done’ property to true.
Generators open up a brand new way of doing lazy evaluation of sequences of results. By using a generator function you can “bookmark” the current position inside of the function without having to manage any external state to keep track of the bookmark.
Generators and Sequences
In this example we are tasked with creating a function that will search through a text for a single word and return the indices of occurrences of that word, solving the problem first without generators, and then with generators.
Before
The non-generator solution we came up with will search through the entire text and return all of the indices of the word we are looking for in a single array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function findWord(text, wordToFind) {
var positions = [];
var totalPosition = 0, currentPosition;
do {
currentPosition = text.indexOf(wordToFind);
if (currentPosition !== -1) {
positions.push(totalPosition + currentPosition);
text = text.substr(currentPosition + wordToFind.length);
totalPosition = totalPosition + currentPosition + wordToFind.length;
}
} while (currentPosition !== -1);
return positions;
}
var positions = findWord('How much wood could a woodchuck chuck if a woodchuck could chuck wood?', 'wood');
var firstTwoPositions = [positions[0], positions[1]]; // [9, 22]
After
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* findWordPositions(text, wordToFind) {
var totalPosition = 0;
do {
var currentPosition = text.indexOf(wordToFind);
if (currentPosition !== -1) {
yield totalPosition + currentPosition;
text = text.substr(currentPosition + wordToFind.length);
totalPosition = totalPosition + currentPosition + wordToFind.length;
}
} while (currentPosition !== -1);
}
var positions = findWordPositions('How much wood could a woodchuck chuck if a woodchuck could chuck wood?', 'wood');
var firstTwoPositions = [positions.next().value, positions.next().value]; // [9, 22]
At first it might not appear that the code changed very much, but there is a very important difference here. Instead of looping through the text finding every single occurrence of the target word, it instead yields after it finds a single match. When you resume it later (by calling .next()), it continues from where it left off and searches for the next spot. This could be very useful if you have a very large text to search with many occurrences of the target word but you only need to find the first few – just call .next() as many times as you need.
Async behavior using Generators and Promises
Although using a generator function by itself may be nice for lazy evaluation of sequences, in my opinion it’s actually rather hard to come up with a practical example that isn’t just re-implementing fibonacci with generators. However, by using generators, promises, and a third party coroutine library such as co you unlock what I think is one of the biggest potentials of generators – writing asynchronous code as if it were synchronous code.
Many of you have probably experienced what is known as “callback hell” – where one asynchronous function needs the result of another asynchronous function which may call a couple of other asynchronous functions and before you know it you have a function that’s nested ten levels deep and you don’t know how you got there. Promises alone are helpful in saving you from some of that pain, but combined with generators they can be even more powerful.
The following examples will solve the following problem: you want to send a message to all of a single user’s mutual contacts (users who also have the first user as a contact) To do that you must first fetch the user, then iterate over the user’s contact list, fetch all of the those contacts’s contacts, verify that they also have the original user in their contact list, and then send the message to each of them.
In each example, assume that there is a built in “fetch” (hey, there is one!) function that makes an http request and returns a promise that is resolved when the data returns.
Before
This example will use promises alone to solve this problem:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// makes an http request to fetch the user's contact list and returns a Promise that when resolved returns the user's contacts
// the Promise result is structured as follows: { userId: <Number>, contacts: Array<Number> }
// where the contacts array contains all of the ids of the user's contacts
function getUserContacts(userId) {
return fetch('/users/' + userId + '/contacts');
}
function sendMessage(fromUserId, toUserId, message) {
var url = '/users/' + toUserId + '/messages'; // /users/42/messages
var body = { from: fromUserId, message: message};
return fetch(url, { body: body, method: 'POST' });
}
function sendMessageToMutualContacts(userId, message) {
getUserContacts(userId)
.then(function (user) {
return Promise.all(user.contacts.map(getUserContacts))
})
.then(function (contacts) {
var mutualContacts = contacts.filter(function (contact) {
return contact.contacts.indexOf(userId) !== -1;
});
var sends = Promise.all(mutualContacts.map(function (contact) {
return sendMessage(userId, contact.userId, message);
}));
return sends;
})
.then(function (sends) {
console.log('sent messages to ' + sends.length + ' users');
})
.catch(function (error) {
console.error('There was an error sending messages', error);
});
}
Let’s break down this example so that we can better understand what’s happening in each section:
1
2
3
4
getUserContacts(userId)
.then(function (user) {
return Promise.all(user.contacts.map(getUserContacts));
})
First we’re calling the function getUserContacts which is making an asynchronous request and returning a Promise which will contain a list of the user’s contacts when it is resolved.
When it is resolved, we loop over the user’s contacts and begin to fetch all of the contacts of those contacts. The .map function is passed a function that is returning a promise (the contacts for that user) so we end up with an array of promises.
Promise.all is used to wait for the completion of all of those promises, which itself returns a promise.
1
2
3
4
5
6
7
8
9
10
11
.then(function (contacts) {
var mutualContacts = contacts.filter(function (contact) {
return contact.contacts.indexOf(userId) !== -1;
});
var sends = Promise.all(mutualContacts.map(function (contact) {
return sendMessage(userId, contact.userId, message);
});
return sends;
})
When the Promise.all is resolved we’ll have an array of contact objects for the original user’s contacts.
The next step is to only pick the mutual contacts, so we do a filter to remove contacts that don’t have the sending user in their contact list.
Taking those mutual contacts, we then use a similar technique we used to fetch multiple contacts at once by passing in a function to map which will return a promise – this time one that actually performs the message send.
Again, we use Promise.all to create a new promise that is resolved when all of the message sends are also resolved.
1
2
3
4
5
6
.then(function(sends) {
console.log('sent messages to ' + sends.length + ' users');
})
.catch(function(error) {
console.error('There was an error sending messages', error);
});
Finally, we wait for the completion of all message sends and then log to the console the number of messages we sent. Additionally, if any of the steps fail we have a .catch handler to log the error.
You can see in this example that even though Promises allow for a pretty clean implementation, there is still a bit of mental gymnastics that you have to go through to unravel exactly what is going on and what is being called in what order – there are a whole bunch of “thens” and Promise.all to keep track of.
After
With generators and a coroutine library wrapping everything, one can write code as if it were closer to synchronous code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getUserContacts(userId) {
return fetch('/users/' + userId + '/contacts');
}
function sendMessage(fromUserId, toUserId, message) {
var url = '/users/' + toUserId + '/messages'; // /users/42/messages
var body = { from: fromUserId, message: message};
return fetch(url, { body: body, method: 'POST' });
}
function sendMessageToMutualContacts(userId, message) {
co(function* () {
try {
var user = yield getUserContacts(userId);
var contacts = yield user.contacts.map(getUserContacts);
var mutualContacts = contacts.filter(contact => contact.includes(userId));
var sends = yield mutualContacts.map(function (contact) {
return sendMessage(userId, contact.userId, message);
});
console.log(`sent messages to ${sends.length} users`);
return sends;
} catch (error) {
console.error('There was an error sending messages', error);
}
});
}
Notice that Promises are not eliminated here – in fact they work together with generators and the co-routine library to achieve the result we want. However, the code looks much simpler than before – in fact we were able to eliminate all of the ‘.then’ and ‘.catch’ handlers and their resulting callbacks and treat things almost as if it’s being handled synchronously.
Again, let’s break down the code:
1
2
co(function* () {
try {
This line is where the magic starts – it is calling our coroutine library, co, and passing it a generator function.
co will call the function you pass to it and then manage the resulting generator and the values that are yielded to it to allow you to write the synchronous style of code. co lets you yield single promises as well as arrays of promises. When a single promise is yielded to it, it waits for that promise to be resolved and then passes that result to the code that yielded.
When an array of promises is yieled, it acts like Promise.all and only continues when all of the promises in the array are also resolved.
One other important thing to notice is that we can use try-catch instead of having to use a promise .catch handler. When a promise is rejected or an error is thrown, co will rethrow that error so it can be caught in a normal try-catch block.
1
2
3
var user = yield getUserContacts(userId);
var contacts = yield user.contacts.map(getUserContacts);
// at this point, contacts is just a normal array like: [ { userId: 2, contacts: [1, 3] }, { userId: 3, contacts: [2] } ]
These two lines look super simple compared to the promise-d version! We call the getUserContacts function and yield the resulting promise. When that result returns it is assigned to the user variable just like a normal synchronous assignment. It is very important to understand that this is NOT blocking the JavaScript event loop when we do this. Other code that we might have called before or after the sendMessageToMutualContacts function will continue to run, it’s just inside of the generator function where we’re paused waiting for the result.
After we get the user’s contacts, we once again fetch all of those contact’s contacts by mapping it into an array of promises. However, because co accepts an array of promises as something we can yield to it, we do not need to wrap it in Promise.all. Once all of those contacts ares fetched, it is assigned to the contacts variable like normal.
1
2
3
4
var mutualContacts = contacts.filter(contact => contact.includes(userId));
var sends = yield mutualContacts.map(function (contact) {
return sendMessage(userId, contact.userId, message);
});
To filter the mutual contacts in a super clean way, we’re using fat arrow functions and the ES6 Array function .includes which returns true if the given item is in the array.
Then we do the same trick we did with fetching the users in bulk except this time with sending the message. The result of all of our message sends is placed into the sends array.
1
2
3
4
5
console.log(`sent messages to ${sends.length} users`);
return sends;
} catch (error) {
console.error('There was an error sending messages', error);
}
Finally, instead of having to add another .then and a .catch to gather the results of the send or log any errors from our method, we can just return it like normal and have a catch block to gather our errors.
Hopefully this showed you some of the power of generators – but we still have to include a third party library and wrap all of our code in these co() calls. Wouldn’t it be nice if this was built in natively? Enter async and await.
Async and Await
The idea of writing asynchronous code in a synchronous fashion is so alluring that many programming languages bake the feature right into the language. Unfortunately, ES6 does not include such a feature (although as we saw we can get close with promises and generators).
We’re about to enter one of the “Beyond” parts of this blog series – a feature that is currently proposed for ES7 (also known as ES2016). This feature is known as async/await.
Warning - the ES7 specification is not standardized, and as such the syntax presented here is subject to change.
Because this feature is not standardized yet, it’s not available in any major browser as of this posting. However, you can still use it! Use a transpiler like Babel to turn the ES7 code into usable ES6 or ES5 code (Babel supports turning async/await into ES5 compatible code through use of an injected library that simulates it).
Look ma, no coroutine library!
Async and await are two new JavaScript keywords that work together in tandem to eliminate the need for a third party coroutine library. Instead, you can use async and await in the places that you used co, function*, and yield.
The “async” keyword is placed BEFORE the function keyword in a function definition and is used to indicate that await will be called inside of it. This is not optional – you cannot call await inside a function that is not marked with the async keyword.
The “await” keyword is what actually performs the “waiting”. It essentially replaces the “yield” keyword that was used in our co example.
You are allowed use the await keyword on a Promise, and only a Promise – unlike with co you cannot currently await an array of promises. To achieve the same effect you simply have to call Promise.all on the array of promises and await on that instead.
1
2
3
4
async function getAllUsers(userIds) {
var users = await Promise.all(userIds.map(getUser)); // waits
return users;
}
Let’s see how it’s used in a real example – the same example we encountered earlier about sending messages to a user’s mutual contacts.
Before
Note: there are no changes from before – this example is copy/pasted so that you can more quickly compare them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getUserContacts(userId) {
return fetch('/users/' + userId + '/contacts');
}
function sendMessage(fromUserId, toUserId, message) {
var url = '/users/' + toUserId + '/messages'; // /users/42/messages
var body = { from: fromUserId, message: message};
return fetch(url, { body: body, method: 'POST' });
}
function sendMessageToMutualContacts(userId, message) {
co(function* () {
try {
var user = yield getUserContacts(userId);
var contacts = yield user.contacts.map(getUserContacts);
var mutualContacts = contacts.filter(contact => contact.includes(userId));
var sends = yield mutualContacts.map(function (contact) {
return sendMessage(userId, contact.userId, message);
});
console.log(`sent messages to ${sends.length} users`);
return sends;
} catch (error) {
console.error('There was an error sending messages', error);
}
});
}
After
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function getUserContacts(userId) {
return fetch('/users/' + userId + '/contacts');
}
function sendMessage(fromUserId, toUserId, message) {
var url = '/users/' + toUserId + '/messages'; // /users/42/messages
var body = { from: fromUserId, message: message};
return fetch(url, { body: body, method: 'POST' });
}
async function sendMessageToMutualContacts(userId, message) {
try {
var user = await getUserContacts(userId);
var contacts = await Promise.all(user.contacts.map(getUserContacts));
var mutualContacts = contacts.filter(contact => contact.includes(userId));
var sends = await Promise.all(mutualContacts.map(function (contact.userId) {
return sendMessage(userId, contact.userId, message);
}));
console.log(`sent messages to ${sends.length} users`);
return sends;
} catch (error) {
console.error('There was an error sending messages', error);
}
}
This code didn’t really change much from before: * We got rid of our generator function and replaced it with “async”. * We replaced yields with awaits. * We used Promise.all in places where we were yielding arrays. * We no longer have to wrap everything in the call to our coroutine library. Because you would have to do that every time you interacted with asynchronous code, that can lead to a great deal of clutter – but with async/await that is eliminated and you’re free to just focus on the business logic.
It’s important to point out that we didn’t make all of our functions async. We still need our getUserContacts and sendMessage function to return promises so that we can await them, so there would be no real benefit to also marking those as async and awaiting inside of them.
A decent rule to follow is that if you’re just calling one asynchronous function and returning the result, it can probably stay as a Promise.
One final comment is that the result of an async function is just a Promise with the returned result as the resolved value – so it can be consumed like a promise or consumed by another async/await function.
Conclusion
ES6 and ES7 have some very powerful new features in the form of Promises, Generators, and async/await. In fact, Generators and async/await have the potential to completely change the way that we write code today. Once async/await gets a bit further along on the path to standardization, I’m positive we’ll see more libraries and frameworks spring up to support that style of programming.
In the next part of “ES6 and Beyond” we’ll dig into a brand new feature in ES6 that is very controversial, but useful: classes.