Generators, Async/Await, and Koa.js

Taming promise chains and easy migrations for the future

by Chris Griffing

Warning!!!

No Semicolons.

Aimed at Node.js

On the client-side, just use Babel for cross-browser compatibility.

Server-side debugging with source maps is tricky according to some and fine according to others.

Callback Hell


app.get('/', function (req, res) {
  Widgets.createWidgets(20, function (widgets) {
    Gadgets.createGadgetsFromWidgets(widgets, function (gadgets) {
      Things.createThingsFromGadgets(gadgets, function (things) {
        res.set('Content-Type', 'application/json')
        res.send(JSON.stringify(things))
      })
    })
  })
})
                        

Not the messiest example

A Little Better



app.get('/', function (req, res) {
  Widgets.createWidgets(20, widgetsCallback)   
                              
  function widgetsCallback (widgets) {
    Gadgets.createGadgetsFromWidgets(widgets, gadgetsCallback)
  }
  function gadgetsCallback (gadgets) {
    Things.createThingsFromGadgets(gadgets, thingsCallback)
  }
  //res is available from the Express.js server endpoint
  function thingsCallback (things) {
    res.set('Content-Type', 'application/json')
    res.send(JSON.stringify(things))
  }
})
                        

Flatter but harder to follow (arguably)

Long Promise Chains


app.get('/', function (req, res) {
  Widgets.createWidgets(20)
    .then(function (widgets) {
      return Gadgets.createGadgetsFromWidgets(widgets)
    }).then(function (gadgets) {
      return Things.createThingsFromGadgets(gadgets)
    }).then(function (things) {
      res.set('Content-Type', 'application/json')
      res.send(JSON.stringify(things))
    }).catch(function (error) {
      //handle errors
    })
})
                    

Much better

Generators: Quick Example of an Express endpoint with a Generator


app.get('/', function (req, res) {
  async(function * (next) {
    try {
      let widgets = yield Widgets.createWidgets(20)
      let gadgets = yield Gadgets.createGadgetsFromWidgets(widgets)
      let things = yield Things.createThingsFromGadgets(gadgets)
      res.set('Content-Type', 'application/json')
      res.send(JSON.stringify(things))
    } catch (e) {
      //handle errors
    }
  })
})
                        

This lets us yield each promise in a synchronous fashion without blocking the thread from executing other code.

Generators explained


app.get('/', function (req, res) {
  async(function * (next) {
    try {
      let widgets = yield Widgets.createWidgets(20)
      let gadgets = yield Gadgets.createGadgetsFromWidgets(widgets)
      let things = yield Things.createThingsFromGadgets(gadgets)
      res.set('Content-Type', 'application/json')
      res.send(JSON.stringify(things))
    } catch (e) {
      //handle errors
    }
  })
})

                        

Generators: Basics


function * evenNumbers () {
  for(var i = 1; true; i++) {
    if(i % 2 === 0) {
      yield i
    }
  }
}
                    

> x = evenNumbers()
  evenNumbers {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
> x.next()
  Object {value: 2, done: false}
> x.next()
  Object {value: 4, done: false}
> x.next()
  Object {value: 6, done: false}
> x.next()
  Object {value: 8, done: false}
> x.next()
  Object {value: 10, done: false}
                    

Generators (stoppable)

Definition:
function * evenNumbersStoppable () {
  for(var i = 1; true; i++) {
    if(i % 2 === 0) {
      var stop = yield i
      if(stop === true) {
        return i
      }
    }
  }
}
                    
Output:
> y = evenNumbersStoppable()
  evenNumbersStoppable {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
> y.next()
  Object {value: 2, done: false}
> y.next()
  Object {value: 4, done: false}
> y.next()
  Object {value: 6, done: false}
> y.next()
  Object {value: 8, done: false}
> y.next(true)
  Object {value: 8, done: true}
> y.next()
  Object {value: undefined, done: true}
> y.next()
  Object {value: undefined, done: true}
                    

To stop a generator, return a value instead of yielding it.

Generators (throwing)


> x = evenNumbersStoppable()
  evenNumbersStoppable {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
> x.next()
  Object {value: 2, done: false}
> x.next()
  Object {value: 4, done: false}
> x.throw(new Error('Something has gone terribly wrong here'))
  Uncaught Error: Something has gone terribly wrong here(…)(anonymous function) @ VM6460:2InjectedScript._evaluateOn @ (program):878InjectedScript._evaluateAndWrap @ (program):811InjectedScript.evaluate @ (program):667
> x.next()
  Object {value: undefined, done: true}
                    

Generators have a built in function for throwing errors and stopping itself.

Generators (wrapping promises)


// copied (and lightly modified) from: https://www.promisejs.org/generators/
function async (makeGenerator){
  return function () {
    //start the generator function
    var generator = makeGenerator.apply(this, arguments)
    
    try {
      return handle(generator.next())
    } catch (ex) {
      return Promise.reject(ex)
    }

    function handle (result){
      // result => { done: [Boolean], value: [Object] }
      if (result.done) return Promise.resolve(result.value)

      return Promise.resolve(result.value).then(function (res){
        return handle(generator.next(res))
      }, function (err){
        return handle(generator.throw(err))
      });
    }
  }
}
                    

This wrapper allows us to yield the result of a promise or handle the error of it with a try/catch.

Generators (wrapping promises): examples

A Sequence of Operations:
var get = async(function * (){
  var left = yield readJSON('left.json')
  var right = yield readJSON('right.json')
  return {left: left, right: right}
})
                    
Parallel Operations:
var get = async(function * (){
  var left = readJSON('left.json')
  var right = readJSON('right.json')
  return {left: yield left, right: yield right}
})
                    

Async/Await: examples

A Sequence of Operations:
var get = async function (){
  var left = await readJSON('left.json')
  var right = await readJSON('right.json')
  return {left: left, right: right}
}
                    
Parallel Operations:
var get = async function (){
  var left = readJSON('left.json')
  var right = readJSON('right.json')
  return {left: await left, right: await right}
}
                    

Looking similar? There is a reason.

Async/Await Coming Soon

  • Uses wrapped generators for promise-based control flow.
  • Originally slated as a feature for ES2016, but was delayed.
  • Currently at Stage-3 of spec approval. Already supported by Chakra (The Microsoft js engine), so only one other vendor needs to support it to get it to Stage-4.

The Spec

https://tc39.github.io/ecmascript-asyncawait/

Desugaring


async function <name>?<argumentlist><body>
                        

function <name>?<argumentlist>{ return spawn(function*() <body>, this); }
                        

async desugars into a promise handling wrapped generator.

Desugaring: *spawn


function spawn(genF, self) {
    return new Promise(function(resolve, reject) {
        var gen = genF.call(self);
        function step(nextF) {
            var next;
            try {
                next = nextF();
            } catch(e) {
                // finished with failure, reject the promise
                reject(e);
                return;
            }
            if(next.done) {
                // finished with success, resolve the promise
                resolve(next.value);
                return;
            }
            // not finished, chain off the yielded promise and `step` again
            Promise.resolve(next.value).then(function(v) {
                step(function() { return gen.next(v); });
            }, function(e) {
                step(function() { return gen.throw(e); });
            });
        }
        step(function() { return gen.next(undefined); });
    });
}
                        

What about Koa?

  • Originally made by TJ Hollowaychuk
  • Middleware is already wrapped to support easy promise-based generator flows.
  • Koa2 beta is stable, ready, and waiting for async/await support in Node.

Migrating: From Express to Koa

Convert a normal middleware function to a generator.

Migrating: From then to yield

Looking much cleaner.

Migrating: From yield to await

So Simple. This part of the process could probably even be automated.

More about Koa.js

It is low level like Express.

    Frameworks and wrappers for more functionality:
  • Strapi - http://strapi.io - Full-featured (users/permissions, GraphQL support and much more). Uses Waterline ORM. Has a Studio GUI similar to Loopback.
  • [On Hold/Alpha]Koala - https://github.com/koajs/koala - A wrapper for Koa with HTTP2 support, JSONP, Response caching, and more. On hold and waiting for more general underlying HTTP2 support like from Nginx.

Much more info about Koa on the wiki on Github: https://github.com/koajs/koa/wiki

Caveats

  • Generators and async/await are currently unable to be optimized by compiler.
  • Many plugins and middleware are not ready for Koa 2.0.
  • Koa1 middleware can be wrapped with koa-convert.
  • koa-websocket, koa-stream, and similar libraries work differently than standard middleware so koa-connect is unlikely to work for that functionality.

Wink, Wink, Nudge, Nudge

Citations and Further Reading