@tlhunter@mastodon.social

The Long Road to Async/Await
in JavaScript

Thomas Hunter II

Distributed Systems with Node.js: bit.ly/34SHToF

Evolution of Asynchronous Control Flow

  • Phase 1: Callbacks
  • Phase 2: Promises
  • Phase 3: Generators/Yield
  • Phase 4: Async/Await

Phase 0: Synchronous Code

Synchronous Code (Single Stack)

let result = sendMessage('tlhunter', 'hi')
console.log(result)

function sendMessage(userId, message) {
  let user = getUser(userId)
  let able = canSend(user)
  if (!able) return false
  return writeMessage(user, message)
}

Synchronous Code Error Handling

try {
  var result = sendMessage('tlhunter', 'hi')
} catch (err) {
  return console.error(err)
}
console.log(result)

function sendMessage(userId, message) {
  throw new Error('Bad Stuff')
}

Phase 1: Callbacks

Callbacks

sendMessage('tlhunter', 'hi', ⏰(err, result) => {
  console.log(result)
})

function sendMessage(userId, message, cb) {
  getUser(userId, ⏰(err, user) => {
    canSend(user, ⏰(err, able) => {
      if (!able) { cb(null, false); return }
      writeMessage(user, message, cb)
    })
  })
}

Callback Error Handling

sendMessage('tlhunter', 'hi', ⏰(err, result) => {
  if (err) {
    return console.error(err)
  }
  console.log(result)
})

function sendMessage(userId, message, cb) {
  setImmediate(⏰() => cb(new Error('Bad Stuff')))
}

Callback Error Propagation

function sendMessage(userId, message, cb) {
  getUser(userId, ⏰(err, user) => {
    if (err) { cb(err); return } // error check
    canSend(user, ⏰(err, able) => {
      if (err) { cb(err); return } // error check
      if (!able) { cb(null, false); return }
      writeMessage(user, message, cb)
    })
  })
}

Phase 2: Promises

  • ES2015 (ES6)
  • ES5 with Polyfill, e.g. Bluebird
  • Node.js v0.12
  • Chrome 32
  • Firefox 29
  • Safari 7.1
  • Microsoft Edge

Promises

sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
})

function sendMessage(userId, message) {
  return getUser(userId).then(⏰(user) => {
    return canSend(user)
  }).then(⏰(able) => {
    if (!able) return false
    return writeMessage(user, message)
  })
}

Promise Rejection

sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
}).catch(⏰(err) => {
  console.error(err)
})

function sendMessage(userId, message) {
  return Promise.reject(new Error('Bad Stuff'))
}

Phase 3: Generators/Yield

  • ES2015 (ES6)
  • Can Transpile, Cannot Polyfill
  • Node.js v1.0 (aka io.js)
  • Chrome 39
  • Firefox 27
  • Safari 10
  • Microsoft Edge 13

Generators/Yield

let gen = sendMessage('tlhunter', 'hi')
gen.next().value.then(⏰(user) => {
  return gen.next(user).value.then(⏰(able) => {
    return gen.next(able).value.then(⏰(result) => {
      console.log(result)
    })
  })
})
function * sendMessage(userId, message) {
  let user = ⏰yield getUser(userId)
  let able = ⏰yield canSend(user)
  if (!able) return false
  return writeMessage(user, message)
}

Generators/Yield + co

const co = require('co')
sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
})
function sendMessage(userId, message) {
  return co(function * sendMessageGen() {
    let user = ⏰yield getUser(userId)
    let able = ⏰yield canSend(user)
    if (!able) return false
    return writeMessage(user, message)
  })
}

Generators/Yield + co Rejection

const co = require('co')
sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
}).catch(⏰(err) => {
  console.error(err)
})
function sendMessage(userId, message) {
  return co(function * sendMessageGen() {
    ⏰yield Promise.reject(new Error('Bad Stuff'))
  })
}

Phase 4: Async/Await

  • ES2017 (ES8)
  • Can Transpile, Cannot Polyfill
  • Node.js v8.3
  • Chrome 55
  • Firefox 52
  • Safari 10.1
  • Microsoft Edge 13

Async/Await

(async () => {
  let result = ⏰await sendMessage('tlhunter', 'hi')
  console.log(result)
})()

async function sendMessage(userId, message) {
  let user = ⏰await getUser(userId)
  let able = ⏰await canSend(user)
  if (!able) return false
  return writeMessage(user, message)
}

Async/Await Error Handling

(async () => {
  try {
    var result = ⏰await sendMessage('tlh', 'hi')
  } catch(err) {
    return console.error(err)
  }
  console.log(result)
})()

async function sendMessage(userId, message) {
  throw new Error('Bad Stuff')
}

Promise and Async/Await Interop

Async Functions: They're Just Promises!

Async Refactor (Bottom Up)

sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
})

async function sendMessage(userId, message) {
  let user = ⏰await getUser(userId)
  let able = ⏰await canSend(user)
  if (!able) return false
  return writeMessage(user, message)
}

Async Refactor (Bottom Up)

sendMessage('tlhunter', 'hi').then(⏰(result) => {
  console.log(result)
}).catch(⏰(err) => {
  console.error(err)
})

async function sendMessage(userId, message) {
  throw new Error('Bad Stuff')
}

Async Refactor (Top Down)

(async () => {
  let result = ⏰await sendMessage('tlhunter', 'hi')
  console.log(result)
})()
function sendMessage(userId, message) {
  return getUser(userId).then(⏰(user) => {
    return canSend(user)
  }).then(⏰(able) => {
    if (!able) return false
    return writeMessage(user, message)
  })
}

Async Refactor (Top Down)

(async () => {
  try {
    var result = ⏰await sendMessage('tlh', 'hi')
  } catch(err) {
    return console.error(err)
  }
  console.log(result)
})()

function sendMessage(userId, message) {
  return Promise.reject(new Error('Bad Stuff'))
}

Node API Promisification

const util = require('util')
const pifall = require('pifall')
const fs = require('fs')

const readFile = util.promisify(fs.readFile)
pifall(fs)

// Can you spot the anti-pattern?
;(async () => {
  let stuff = ⏰await readFile('stuff.txt')
  let data = ⏰await fs.readFileAsync('data.txt')
  console.log(`${stuff}, ${data}`)
})()

Node API Promisification

const util = require('util')
const pifall = require('pifall')
const fs = require('fs')

const readFile = util.promisify(fs.readFile)
pifall(fs)

;(async () => {
  let [stuff, data] = ⏰await Promise.all([
    readFile('stuff.txt'),
    fs.readFileAsync('data.txt')
  ]);
  console.log(`${stuff}, ${data}`)
})()

Fin