Generators, Coroutines, Async/Await – The future of Javascript

This article assumes you’re already familiar with the basics of callback hell, promises, and the Javascript event loop. For the past few years, promises are rapidly becoming popular. They make it much easier to deal with complicated asynchronous code than callbacks. However, we can do better than promises.

Problems with promises

Promises have several issues that can make large applications difficult to understand, debug, and maintain:

You can’t abort a chain of promises


function switchToPage(page) {
  currentPage = page;
  return fetchPage(page)
    .then(data => {
      if (currentPage !== page) {
        // User already navigated away, discard data.
        let err = new Error('Page switched.');
        err.type = ErrorTypes.PAGE_SWITCHED;
        throw err;
      }
      return data;
    })
    .then(processData)
    .catch(err => {
      if (err.type === ErrorTypes.PAGE_SWITCHED) {
        // Not an actual error, do nothing.
      } else {
        displayErrorMessage(err);
      }
    });
}
In this example, if we don’t want to run the rest of the promise chain, we use a throw to skip the processData. This is obviously not ideal. Another file could chain to the promise returned by switchToPage. That file would have to know how to handle the PAGE_SWITCHED error type as well. This would work fine for a small project that you’re working on alone. However, for larger projects or projects involving multiple people, this makes the codebase error-prone and difficult to maintain.

You often have to resort to scoping to pass data


function displayUser(username) {
  let user = null;
  return getUser(username)
    .then(_user => {
      user = _user;
      return getFriendStatus(user);
    })
    .then(friendStatus => {
      if (friendStatus !== FriendStatuses.FRIENDS) {
        showAddFriendButton();
      }
      displayUser(user);
    });
}
In this example, we need a user local variable because it’s needed in multiple then callbacks. In larger functions, there could be many such variables. It’s harder to split these functions into smaller functions because it’s messy to pass around these variables. You could wrap them in a class, but we’ll see more natural ways of doing it later on.

Promises are harder to read than synchronous code

Here’s production code from one of my projects:

function transaction(queryValuesPairs) {
  return new Promise((succ, fail) => {
    getPool().getConnection((err, conn) => {
      if (err) {
        fail(err);
      } else {
        succ(conn);
      }
    });
  })
  .then(conn => {
    return query('START TRANSACTION', [], conn)
      .then(_ => {
        let promises = queryValuesPairs.map(pair => {
          return query(pair.query, pair.values, conn);
        });
        return Promise.all(promises);
      })
      .then(_ => query('COMMIT', [], conn))
        .catch(err => { return query('ROLLBACK', [], conn)
        .then(_ => {
          throw err;
        });
      });
  });
}
This may be a bit hard to understand, especially if you’re unfamiliar with SQL’s transactions. You have to read it carefully to realize that the caller has to handle all errors. It’s definitely better than implementing it with callbacks. However, compare it with the async/await version:

async function transaction(queryValuesPairs) {
  let conn = await getPool().getConnection(); // Might throw an error.
  await query('START TRANSACTION', [], conn);
  try {
    let promises = queryValuesPairs.map(pair => {
      return query(pair.query, pair.values, conn));
    });
    await Promise.all(promises);
    let val = await query('COMMIT', [], conn);
    return val;
  } catch (err) {
    await query('ROLLBACK');
    throw err;
  }
}
This is half as long and much easier to read. You can clearly tell that nothing is catching the errors thrown at lines 2 and 12. In the rest of this article, I will explain what async and await are, as well as the theory behind them.

Generator functions

Generator functions are part of ES6. They are special functions declared as function*. Generators are commonly used as iterators, but we’ll see that they are also the underlying structure for async/await. Generator functions can be paused by using the yield keyword. yield is like return. However, the next time the generator function runs, it continues from the yield rather than the beginning of the function. Here’s an example of printing all the numbers in a range using normal function versus a generator function:

function range(start, end) {
  return Array(end - start).fill(0).map((_, idx) => start + idx);
}
for (let n of range(0, 100)) {
  console.log(n);
}

function* xrange(start, end) {
  while (start < end) {
    yield start++;
  }
}
for (let n of xrange(0, 100)) {
  console.log(n);
}
The 2 for loops print the exact same thing. However, the xrange function doesn’t have to store an entire array in memory; it generates the next value one at a time. The for of loop implicitly calls the generator’s next method to generate the next value. You can also manually call the next method to generate the values.

let gen = xrange(0, 100);
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
It’s important to know about the next method because you can use it to pass values back into the generator. This allows 2-way communication between your generator and your caller. For example:

function* adder() {
  let n = 0;
  while (true) {
    let val = yield n;
    if (Number(val) === val) {
      n += val;
    }
  }
}

let gen = adder();
gen.next(5);
console.log(gen.next().value()); // 5
gen.next(10);
console.log(gen.next().value()); // 15
You can even call the generator asynchronously:

let gen = adder();
gen.next(5);
console.log(gen.next().value()); // 5
setTimeout(() => {
  gen.next(10);
  console.log(gen.next().value()); // 15
}, 1000);
In this case, the generator yielded a value, waited 1 second, then yielded another value. If you replace the yield in the generator with an await, it starts looking a bit like async/await.

Coroutines

Coroutines are a programming concept that allows functions to pause themselves and give control to another function. The functions pass control back and forth. Coroutines can be implemented using generators and promises. I will show an example of using coroutines and an example of implementing coroutines. Using coroutines to fetch comments for posts would look something like this:

coroutine(function* () {
  let posts = yield fetchPosts();
  let promises = posts.map(post => fetchComments(post));
  let comments = yield Promise.all(promises);
  displayComments(comments);
});
yield pauses the function until fetchPosts gets an Ajax response. Then, the function continues where it left off. This is much easier to read than using callbacks or promises. In this coroutine implementation, the yielded value must be a promise. Here’s a simple coroutine implementation:

function coroutine(fn) {
  let gen = fn();
  let doNext = (data) => {
    let next = gen.next(data);
    if (!next.done) {
      return next.value.then(doNext);
    }
  };
  doNext();
}
As long as the generator only yields promises, next.value will always be a promise. The coroutine calls then on the promise to run doNext again when the promise finishes. When doNext runs again, it unpauses the generator and gets the next promise. This repeats until there’s no more yields in the generator. Each time coroutine calls gen.next, control passes to the generator. Each time the generator calls yield, control returns to coroutine. This concept of passing control back and forth asynchronously is known as coroutine. You can use synchronous control flow with coroutines. In my coroutine example, I didn’t implement error handling. However, proper coroutine implementations let you use try/catch in your generator functions. Some popular coroutine libraries are co and bluebird. Here’s what you can do with co:

function loadPage(path) {
  return co(function* () {
    let page;
    try {
      page = getCached(path)
        ? getCached(path)
        : yield fetchPage(path);
    } catch (err) {
      page = getErrorPage();
    }
    return displayPage(page);
  });
}

showSpinner();
loadPage(path).then(hideSpinner);
This looks almost exactly like async functions. Just replace yield with await and co(function*(){}) with async function(){} and we have async/await.

Async functions

Async functions are just syntactic sugar on top of generators. They use the exact same concept as coroutines. If you don’t understand coroutines yet, then you should reread the coroutines section. Here’s how Babel transpiles async/await. Input:

async function foo() {
  await fetch();
}
Output:

let foo = (() => {
  var _ref = _asyncToGenerator(function* () {
    yield fetch();
  });
  return function foo() {
    return _ref.apply(this, arguments);
  };
})();
All Babel does is replace async functions with calls to _asyncToGenerator.  _asyncToGenerator is a coroutine implementation that Babel adds. Even though async functions and coroutines are essentially the same thing, async/await is a lot more intuitive. At the time of writing, you have to use a transpiler to use async functions. co and bluebird are supported by every popular browser. Unless you have a good reason to not use Babel, you should be using async functions. Async functions are the future of Javascript.

1 thought on “Generators, Coroutines, Async/Await – The future of Javascript”

Leave a Reply

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