This post is about the next generation of asynchronous JavaScript tools and it assumes basic familiarity with promises and callbacks.
In its origin days, JavaScript used callbacks to manage asynchronous flows. Callbacks work but they tend to negatively impact readability. ES6 made Promises a language feature and they do a great job helping developers tame many types of asynchronous patterns. However, despite their many benefits, there is still room for improvement. JavaScript's upcoming asynchronous solutions attempt to make asynchronous code look more synchronous as well as handle streams of asynchronous events. Three of those upcoming tools are:
- Generator functions
- Observables
- Async functions (async/await)
While each of these tools is its own complex topic, this post focuses on the high level functionality they bring to JavaScript.
Generators
Generator functions are an ES6 feature and currently have some browser support. They are supported by Babel and TypeScript if TypeScript is set to ES6 mode.
Generator functions are iterable iterators, which is a fancy way of saying that generator functions return a Generator object with a next() method and developers can use for of loops on Generators. Generator functions are not inherently asynchronous. Generator functions do however provide a mechanism for "pausing" a function.
Here's an extremely simple generator:
function* count(n) {
let current = 0;
while (current >= 0) {
current += 1;
yield current;
}
}
const myCount = count();
console.log(myCount.next().value);
console.log(myCount.next().value);
console.log(myCount.next().value);
console.log(myCount.next().done); // false
This example outputs:
1
2
3
false
This generator represents the infinite sequence of positive integers. Things to notice:
- generator functions are created with a function* syntax
- generator functions return an object with a next() method
- the result of .next() has a value property and a boolean done property
- there is a special keyword yield
- yield produces or "yields" the next().value
What's not necessarily obvious is how yield pauses the function, or "yields" control. Consider the following extra simple example:
function* count(n) {
yield 1;
console.log('test');
yield 2;
}
const myCount = count();
console.log(myCount.next().value);
console.log(myCount.next().value);
const lastNext = myCount.next();
console.log(lastNext.value);
console.log(lastNext.done); // false
This example outputs:
1
test
2
undefined
true
Once yield is called, the generator function pauses. On the next call to next the generator resumes. Also notice that in this example the generator function actually concludes and done is set to true.
Also notice that the last value is undefined. If our generator function had returned a value it would have been in the last .next().value.
These are basic examples of how generators work. There is a lot more that can be done with them. Now that we've looked at the basics we'll examine how yield's pausing capability can be used to tame asynchronous code.
Using yield to Fake Synchronous Flow
The yield mechanism can be leveraged to fake synchronous style code. This is a relatively nuanced mechanism and is best left to a library like Q. Q features a spawn method which allows developers to run "asynchronous" generator functions.
We can implement a simple spawn like method like so:
function spawn(generator) {
const g = generator();
onNext();
function onNext(prev = null) {
const yielded = g.next(prev);
if (!yielded.done) {
yielded.value.then(onNext);
}
}
}
This version of spawn is too simple for any real use. It offers no error handling and lots of things can go wrong. However it's ideal for a small example. Let's see it in action:
spawn(function* fakeSync() {
console.log(
yield new Promise(resolve => setTimeout(() => resolve(1), 250))
);
console.log(
yield new Promise(resolve => setTimeout(() => resolve(2), 500))
);
console.log(
yield new Promise(resolve => setTimeout(() => resolve(3), 750))
);
});
This example will output:
1
2
3
Within our spawn function's generator callback we're able to write code that looks synchronous. This is a very handy feature and it's available in many browsers today. There are a lot of other things that can be done with generators, but for now let's take a look at some other asynchronous tools at our disposal.
Observables
Observables are a proposed language feature and exist today in the form of the RxJS 5 library. RxJS has been around for a while but with it being a proposed language feature, its increasing popularity and its similarity to generator functions it makes sense to include it here.
Observables are inherently asynchronous and they work somewhat like an inverted generator function. Like generator functions they embrace the idea of iteration. Observables do this by providing familiar Array functions like foreEach() and map(). Observables also come with a host of bells and whistles that make working with them quite pleasant.
Basic observable example:
import {Observable} from 'RxJS';
const watchMe = Observable.create((observer) => {
setTimeout(() => observer.next('hello'), 1000);
setTimeout(() => observer.next('world'), 2000);
setTimeout(() => observer.complete(), 3000);
});
// `.subscribe` kicks it all off
watchMe.subscribe(
(msg) => console.log(`Received event message: ${msg}`),
(err) => console.log(`Error: ${err.message}`),
() => console.log('Sequence Complete');
);
In this example watchMe is an Observable. Observables are lazy, meaning nothing happens until .subscribe is called. Once subscribe is called, three handler functions are registered.
- The first fires every time the Observable's internal .next() method is triggered.
- The second fires if the Observable's internal .error() method is triggered.
- The last fires when the Observable's internal .complete() method is triggered; assuming there have been no errors.
This Observable might not look impressive, but the next two examples will show off two features that make Observables incredibly powerful.
Observables Can Clean Themselves Up
The following example demonstrates how Observables can implement their own "tear down" code. This can be incredibly useful.
import {Observable} from 'RxJS';
const watchMe = Observable.create((observer) => {
let timer = setTimeout(() => observer.next('hello'), 1000);
return () => clearTimeout(timer);
});
// `.subscribe` kicks it all off
const subscription = watchMe.subscribe(
(msg) => console.log(`Received event message: ${msg}`)
);
setTimeout(() => subscription.unsubscribe(), 500);
This example has no output. By calling unsubscribe we invoke the method returned at the end of Observable.create() and the timeout that would have logged 'hello' to the console is cancelled.
Observables Work Like Arrays
Not only can Observables clean themselves up, they can be operated on like arrays. This includes the ability to flatMap which can 'unbox' not only arrays, but other Observables.
import {Observable} from 'RxJS';
// This observable represents an HTML5 input that is bound to an
// observable. In a real scenario key presses would trigger `next`
const formData = Observable.create((observer) => {
setTimeout(() => observer.next('some search keywords'), 1000);
});
function search(term) {
// In a real scenario, this would actually use the `term` to query
// some remote API. Instead we'll fake it
return Observable.create((observer) => {
// fake a result
setTimeout(() => observer.next([
'result1', 'result2', 'result3',
]), 1000);
});
}
const searchResults = formData
.filter(Boolean)
.debounceTime(400)
.flatMap((searchTerm) => search(searchTerm))
.flatMap((results) => results)
.map((result, i) => `${i + 1} ${result}`);
searchResults.subscribe(console.log.bind(console));
This example will output:
1 result1
2 result2
3 result3
There's a lot going on here.
- formData is a simple observable that represents a form input box, specifically a search field
- The search function returns an Observable that is supposed to represent the transformed results of an HTTP request
- searchResults represents a series of array transforms to be run against the Observable
In searchResults we see that we are able to use filter to make sure we have "truthy" input coming from the form. If that check passes the form input is also debounced to ensure we don't overload our (fake) API. Then search is called.
The resulting Observable from search is unboxed with flatMap and its result which is an array is also unboxed with another flatMap.
Finally map is called once for each element of the result array. map performs a simple transform on the data.
The interesting part is that none of this happens until .subscribe() is run meaning the entire sequence of array operations can be conveniently subscribed to over, and over, and over.
Just Scratching The Surface
Observables offer a world of options and these examples only begin to address what observables can do. RxJS implements many more operations than those listed here. Additionally the site rxmarbles has wonderful visual examples of observables in action.
Async Functions
Async functions are a proposed language feature and can be used today with Babel. The spec is still subject to change so be careful. Node users may also be interested in node fibers which offer similar benefits but only in node
Async functions are like a "batteries included" version of the way generator functions can make asynchronous code look like synchronous code. They are possibly the simplest of the three tools compared in this post. Async functions take promises and "flatten" them out so that they look synchronous. With async functions promises can even be used in try/catch blocks!
The way async functions currently work is as follows:
- Declare an async function using the async keyword
- Prefix any promise returning functions with await
- Remember the async function declared with async returns a promise
Example:
function someAsyncAction() {
return new Promise((resolve) => {
setTimeout(() => {
const replied = Date.now();
resolve('Hello World!');
}, 2000);
});
}
// note the `async label`
async function syncStyleGoodness() {
const start = Date.now();
// note the `await` keyword
const promiseResult = await someAsyncAction();
console.log(`Result: ${promiseResult}`);
console.log(`Duration: ${Date.now() - start}`);
}
syncStyleGoodness()
.then(() => console.log('End Program: Program Complete'))
.catch((e) => console.log(`Fatal Error: ${e.message}`));
The above code should output something like:
Result: Hello World!
Duration: 2007
End Program: Program Complete
What's important to note is that the function syncStyleGoodness is marked with the async keyword. Within the async function the keyword await is used to ostensibly wait for the promise to resolve putting its result directly into a variable which can then be used. The code is almost purely synchronous looking except that the async function implicitly returns a promise.
What about errors? Let's see:
function someRejectingAction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('test error'));
}, 2000);
});
}
async function syncStyleGoodness() {
try {
const noVal = await someRejectingAction('test error');
console.log('This will never happen!');
} catch (e) {
console.log(`Caught Error: ${e.message}`);
}
}
syncStyleGoodness()
.then(() => console.log('End Program: Program Complete'))
.catch((e) => console.log(`Fatal Error: ${e.message}`));
This will output:
Caught Error: test error
End Program: Program Complete
That's all there is to it. try/catch blocks work around await marked calls just like the would synchronous code. In the event an error is thrown or a rejected promise is not handled, the implicit async promise will reject.
Async functions are pleasantly simple and they allow developers to express asynchronous code in possible the most synchronous looking way.
Summary
JavaScript is maturing into a language with rich asynchronous support. There are many tools to choose from depending on use case. Simple, one time asynchronous calls can (and in many cases should) still be handled with callbacks or promises. Complex series of promises can be "flattened" out into synchronous looking asynchronous code with async functions. Streams of asynchronous actions can be tamed with generator functions or Observables.
With so many options, it can be tempting to want to use all of them - but this is usually unnecessary. Like anything else, take the time to learn how the tools work and then choose the right one for the right task. And always remember to have fun along the way!