Migrate from Co to Async Functions

Kevin Z
4 min readJul 30, 2017

--

Co is a JavaScript library for generator-based control flow. Async function is an ES2017 feature to handle async control flow in JavaScript. Both co and async function achieve the same goal: handle async control flow in a synchronous syntax without using callbacks or Promise-chaining. They also have very similar syntax (example).

Co is a popular library, especially because it is used by web framework Koa in version 1.x. However, as async function is hitting general availability with browsers and node 7.6+, migrating to it can bring the following benefits:

  • Simpler mental model. It is fun to learn about generator, generator functions and how co works. But async function requires a relatively much simpler mental model, which can hopefully lead to higher productivity and fewer mistakes.
  • Better stack trace and debugging experience. Both co and async function work with async call stack in tooling, but using co library leads to more obscure stack trace due to additional wrapping stacks. The native async function gives the optimal stacks that correspond to just your code.
  • One less dependency. Even though co library is small (~250 LOC and 1.2KB minified) and has no dependencies (which is enlightening by itself), removing a dependency and extra wrapping from code is always nice.
  • Be future-proof with libraries and tools. Async function is getting better support and it is foreseeable that co would be replaced eventually in most use cases. For example, koa@2 has already switched to async function.

Migration

You do not have to migrate all at once, but can take steps and migrate one function at a time.

Suppose that you have a generator function

function * foo(bar)

that is being migrated to an async function

async function foo(bar)

Migrate callers

There are a few possible types of callers to foo.

Caller is yielding the call

This implies that the caller is a generator function itself. The first scenario is simple:

// before
yield foo(someBar)
// after
yield foo(someBar) // no migration needed

Since the new foo returns a Promise which is yield-able by co, you do not have to do any thing here.

On the other hand, be careful with the next case:

// before
yield foo
// after
yield foo()

This could be a bit weird because generator functions are also yield-able by co and therefore the before version is equivalent to yield foo() . But the new foo is not a generator function any more, and as a result co would treat it as a thunk. However, our new foo is not a thunk and will never resolve as a thunk would, which means yield foo would never return, leading to a potentially nasty bug. The solution is to yield the result of the function — a promise instead.

Caller is co-wrapping the call

// before
co(foo(someBar))
// after
foo(someBar)
// before
co(foo)
// after
foo()

In the before version, co() runs the old generator / generator function and returns a promise. Since the new foo returns a promise directly, no co() is needed any more.

// before
co.wrap(foo)
// after
foo

co.wrap wraps the old foo (a generator function) into a non-generator function that returns a Promise. For a similar reason as above, since the new foo() returns a Promise directly, no co wrapping is needed.

Caller is using yield *

// before
yield * foo
// after: same as yield

You probably should not have this since co library does not need yield * and in most cases yield would have sufficed. Therefore treating it as yield is likely what you need.

Migrate callees

foo may be calling other functions (i.e., callees) using yield , which need to be migrated since foo is not a generator function any more and therefore cannot yield.

Callee is promise-based

// before
yield somePromise
// after
await somePromise
// before
yield somePromiseReturningFunction
// after
await somePromiseReturningFunction()
// before
yield somePromiseReturningFunction(args)
// after
await somePromiseReturningFunction(args)

These are straight-forward migrations and you are done for good. Congrats!

Callee is generator / generator-function

// before
yield someGeneratorFunction
// after
await co(someGeneratorFunction)
// before
yield someGeneratorFunction(args)
// after
await co(someGeneratorFunction(args))

Since the callees are generator / generator functions (i.e., not migrated yet), we can bridge the gap by using co to turn them into promise-returning functions which can then be await-ed.

Co yield utility

// before
yield [a, b]
// after, assuming a and b are promises
Promise.all([a, b])
// before
yield { a, b }
// after, assuming a and b are promises
Bluebird.props({ a, b })

These are goodies of co around yield that do not directly correspond to await. Therefore we have to handle them more explicitly.

Note: If either of a, b is not a Promise, you have to migrate it specifically. e.g., Promise.all([a, co(b)]).

Further migration

--

--