Let’s get started
If you are working with JavaScript, there’s a good chance that you have a ton of promises or callbacks nested over and over again. Promises helped me clean up the numerous callbacks, but coroutines really took it to the next level. Coroutines allow you to remove callbacks entirely, and write asynchronous code that looks completely synchronous. In a couple of quick steps, I’ll show you how to simplify your promise-based code by converting to coroutines.
Note: This article briefly talks about generators. If you would like a more thorough description, check out my article on generators!
Let’s start with an example
Here’s an example using only promises. I’ve made it a little complex to really show off how powerful coroutines are. Throughout the rest of this post I’ll walk you through the conversion process.
Note: The request
method performs an HTTP GET request on a url, and returns a Promise
.
Example 1
function GET() {
// Make an HTTP GET request to http://www.dashron.com
return request('http://www.dashron.com')
.then(function (json) {
// parse the response
json = JSON.parse(json);
// Request a couple more web pages in response to the first request
return Promise.resolveAll([
request('http://www.dashron.com/' + json.urls[0]),
request('http://www.dashron.com/' + json.urls[1])
])
.then(function (pages) {
// Build the response object
return {
main: json,
one: pages[0],
two: pages[2]
};
});
})
.catch(function (error) {
// Handle errors
console.log(error);
});
}
// When the GET request is complete, log the response, which is a combination of all responses
GET().then(function (response) {
console.log(response);
});
Create a coroutine
First you need to create a coroutine. I’ve written a library (roads-coroutine) that helps you build coroutines. This library exposes a function, which takes a generator function as its only parameter and returns a coroutine.
Example 2
var coroutine = require('roads-coroutine');
var GET = coroutine(function* GET() {
// ... Removed for brevity
});
GET().then(function (response) {
console.log(response);
});
Find your promises, and add yield statements
Find all your promises, and throw yield
directly in front (without removing anything!). yield
is a keyword that can only be used in generators. In the below example, it was added before request
and Promise.resolveAll
. When everything is done, yield acts like an asynchronous equals sign. It will wait until the promise is resolved, and pass the result to the left. If the promise is rejected, yield will throw the appropriate error.
Example 3
var coroutine = require('roads-coroutine');
var GET = coroutine(function* GET() {
// Make an HTTP GET request to http://www.dashron.com
return yield request('http://www.dashron.com')
.then(function (json) {
// parse the response
json = JSON.parse(json);
// Request a couple more web pages in response to the first request
return yield Promise.resolveAll([
request('http://www.dashron.com/' + json.urls[0]),
request('http://www.dashron.com/' + json.urls[1])
])
.then(function (pages) {
// Build the response object
return {
main: json,
one: pages[0],
two: pages[2]
};
});
})
.catch(function (error) {
// Handle errors
console.log(error);
});
});
// When the GET request is complete, log the response, which is a combination of all responses
GET().then(function (response) {
console.log(response);
});
Kill your promise handlers
Now that yield
handles your promise functions for you (by returning the result, and throwing the reject), you can just use normal variables and try/catch. In example 4 I remove all then
and catch
statements, and replace them with variables and a try/catch.
Example 4
var GET = coroutine(function* GET() {
try {
// Make an HTTP GET request to http://www.dashron.com
var json = yield request('http://www.dashron.com');
// parse the response
json = JSON.parse(json);
// Request a couple more web pages in response to the first request
var pages = yield [
request('http://www.dashron.com/' + json.urls[0]),
request('http://www.dashron.com/' + json.urls[1])
];
return {
main: json,
one: pages[0],
two: pages[2]
};
} catch (error) {
// Handle errors
console.log(error);
};
});
// When the GET request is complete, log the response, which is a combination of all responses
GET().then(function (response) {
console.log(response);
});
Notice all nesting is gone. Instead of making more requests in the then
of the first promise, yield
will handle the waiting for you. The above code looks synchronous, and is much easier to read.
Aesthetics aside, this solves one major headache with promises. With promises, if you ever forget a catch, your error will be ignored, and lost forever (or caught by the hard-to-manage uncaughtRejectionHandler
. With coroutines, your exceptions will be thrown as expected, and can be processed as you see fit.
Let’s make it official
ECMAScript 7 is adding two new keywords to support this feature natively. async
functions instead of generators, and the await
keyword instead of yield
.
async function () {
console.log(await request("http://dashron.com"));
}
There are some minor differences which make this system better (such as order of operations) but we have a while until it will be available for use. In the meanwhile, keep using roads-coroutine
!