Async functions
in practice

tips, tricks and caveats you want to know
before migrating from callbacks


Miroslav Bajtoš
Lead Node.js Engineer @ IBM

LoopBack Logo

The API Framework for Node.js

http://v4.loopback.io

😱

(me in 2016/2017)

👌

(me in 2018)

Agenda

  • async and await are awesome!
  • are they ready for prime time?
  • practical aspects

async & await

and its goodness

Definition

An async function can contain an await expression
that pauses the execution of the async function
and waits for the passed Promise's resolution,
and then resumes the async function's execution
and returns the resolved value.

Source: Mozilla web docs

Example


      async function checkStatus(){
        const response = await request('http://example.com/');
        return response.statusCode === 200;
      }
    

callback style


        function checkStatus(callback){
          request('http://example.com/', (error, callback) => {
            if (error) return callback(error);
            callback(null, response.statusCode === 200);
          });
        }
      

It works great with promises!

consume promises


    function sleep(ms) {
      return new Promise((resolve, reject) => {
        setTimeout(resolve, ms);
      });
    }
    

    await sleep(100);
    

produce promises


    async function respondToQuestion() {
      await sleep(500); // (think)
      return 'my answer';
    }
    

    respondToQuestion()
      .then(result => console.log(result));
    

Better error stack traces

callbacks


    function step(cb) { process.nextTick(cb); }

    function walk(cb) {
      return step(err => {
        cb(new Error('too tired'));
      });
    }
    

    Error: too tired
        at step (callback.js:5:8)
        at process._tickCallback (next_tick.js:61:11)
    

promises


    function step() { return Promise.resolve(); }

    function walk() {
      return step().then(() => {
        throw new Error('too tired');
      });
    }
    

    Error: too tired
        at step.then (promise.js:5:11)
        at process._tickCallback (next_tick.js:68:7)
    

async & await


    async function step() {}

    async function walk() {
      await step();
      throw new Error('too tired');
    }
    

    Error: too tired
        at walk (async.js:5:9)
        at process._tickCallback (next_tick.js:68:7)
    

The debugger

F10 steps over await statements

zero-cost async stack traces

coming soon to Node.js 11.x

$ node --async-stack-traces index.js

https://v8.dev/blog/fast-async#improved-developer-experience

Implementation status

The runtime


January 2017 ES2017
February 2017 Node.js 7.6.0
October 2017 Node.js 8.9.0 LTS

Node.js core API

work in progres

(see nodejs/node#15413)

Node.js  8  (LTS)


util.promisify()


(more on this later)

Node.js  10

experimental API

require('fs').promises

require('dns').promises

Ecosystem readiness

  • server frameworks
  • HTTP clients
  • ORMs
  • test frameworks

Performance

Microbenchmark

https://github.com/strongloop-forks/bluebird/tree/async/await

Promisified core APIs

A "real" app

https://github.com/bajtos/async-frameworks

5 tips & tricks

1. Consume callbacks


    const util = require('util');
    const fs = require('fs');
    const readFile = util.promisify(fs.readFile);

    async function readData() {
      return await readFile('data.txt', 'utf-8');
    }
    

2. await events


    const http = require('http');
    const pEvent = require('p-event');

    async function start(handler) {
      const server = http.createServer(handler);
      server.listen(0);

      await pEvent(server, 'listen');
      return `http://localhost:${server.address().port}/`;
    }

    start(/*handler*/).then(
      url => console.log('Listening on:', url),
      err => console.log('Cannot start the server:', err)
    );
    

3. produce callbacks

async functions in callback-based code

Can you spot two problems?


    async function step() {
      return 'some data';
    }

    app.use((req, res, next) => {
      step()
        .then(next)
        .catch(next);
    });
    

the right way™


    app.use((req, res, next) => {
      step()
        .then(
          // onFulfilled
          success => next(),
          // onRejected
          next
        )
        .catch(
          // crash if callback throws
          err => process.nextTick(() => { throw err; })
        });
    });
    

4. parallel vs. serial

await executes tasks serially


    const walkResult = await walk();
    const talkResult = await talk();
    

use Promise.all() to run in parallel


    const [walkResult, talkResult] = await Promise.all([
      walk(),
      talk(),
    ]);
    

5. how to map array values


    const sources = [
      'http://example.com',
      'http://nodesummit.com'
    ];

    // download all URLs
    const result = source.map(
      async (url) => await request(url)
    );
    // result is an array of promises!

    const actualResult = await Promise.all(result);
    

3 Caveats

1. Error handling

unhandled errors (callbacks)


    function tick(callback) {
      throw new TypeError('undefined is not a function');
    }

    tick(console.log);
    

unhandled errors (async)


    async function tick() {
      throw new TypeError('undefined is not a function');
    }

    // missing await or then
    tick();
    

logs a warning, does not crash (yet)

process.on('unhandledRejection')

user-land Promise libraries


How to detect unhandled rejections?

2. async function returns
a native promise


    const Bluebird = require('bluebird');
    async function run() {
      return Bluebird.resolve(1);
    }

    const result = run();

    console.log(result instanceof Bluebird)
    // false
    console.log('spread' in result);
    // false
    

3. await before returning
or not?


    async function getRepos() {
      const url = 'https://api.github.com/users/bajtos/repos';

      // GOOD: return the promise immediately
      return request(url);

      // NOT RECOMMENDED: await the result before returning
      return await request(url);
    }
    

avoid promise rewrapping

Migration guide

Complete rewrite is almost never

the right thing to do.

Keep delivering value to your clients.

Work incrementally.

Focus on areas that changes most often.

Use your test suite to drive the priorities.

  1. Keep support for callbacks for now.
  2. Enable async/await consumers: promisify APIs.
  3. Eventually rework the implementation from callbacks to async functions.

Key takeaways

async functions are ready to use

learn the new programming model

(and its caveats)

upgrade your code base incrementally


Thank you!


Miroslav Bajtoš

github.com/bajtos


(slides: bajtos.net/T)