Day 6: async/await

We would all wait for two thousand years as a Roman guarding the person we loved, but when it comes to Ajax requests, even a few ms is much too long to wait. No one wants an app that takes two thousand years to respond. If we want a responsive app we are forced to make an async dance to avoid JavaScript's blocking nature. Nothing that runs longer than 50ms or so can be tolerated. Every millisecond consumed is a moment the UI cant do anything else.

When your code is small and tiny you can accomplish most of your needs through a simple callback. Pass in a function and later it will get called again with the data you requested. Unfortunately, after a certain point callbacks fall apart like a swarm of nanobots suddenly losing power. You can only have so many callbacks before code becomes a mess. You need a Big Hero to stop those callbacks and bring sanity.

In an age of complex single page apps we needed a better way to handle asynchronous code. The better way came in the form of promises.

Promises promise to return data one day. You get an object back and you can add listeners to it and then when the data comes back your listeners get called. This let's you use a value and act like you have it before you do. It is powerful. They look like this:

fetch('/users.html')
  .then(function(response) {
    return response.text()
  }).then(function(body) {
    document.body.innerHTML = body
  })

Promises have a very small API surface. All the handlers for what a promise offers are just chain method calls. We add callbacks to a promise by using then. To handle an error you add a catch callback:

fetch('/users.json')
  .then(function(response) {
    return response.json()
  }).then(function(json) {
    console.log('parsed json', json)
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

Promises are Just Objects

Promises can seem like this mysterious API implemented by your browser, a magic trick you can make use of but never fully understand. Most frustration with promises is born out of this view. We see the promises as something we cant ever understand so we don't peek beneath the covers to truly understand how they work.

Promises at their core are not complicated at all. They are not some strange browser API, the browser merely adopted the standard. We can see that Promises are just normal objects:

const promise = new Promise((resolve) => {
  const ship = {
    name: "Time And Relative Dimension In Space",
  }

  setTimeout(() => {
    resolve(ship)
  }, 1000)
})

console.dir(promise)

Running this code (in a browser) will give us something like this:

Promise__proto__:
  catch: catch()
  constructor: Promise()
  then: then()
Object[[PromiseStatus]]: "resolved"
[[PromiseValue]]: Object

See there is nothing special or magical about Promises. They are just objects and a standard. We could even build our own!

Building Your Own Promise Implementation

Promises basically have two API areas an "inside" and an "outside". The inside has a resolve and reject method that we use to pass values and errors back to the outside when the promise is complete. The outside has a then method that will be called with any values and a catch method that will be called with any errors.

The inside API is handled when we create the promise. The promise gets a callback to its constructor and then we pass in the resolve and reject methods to that callback.

The outside API is really simple, and essentially all we have to do is keep track of the thens and catches then call them from resolve or reject. We can likely generalize the call into a single complete method that calls all the callbacks we have promised values to. Not too hard. Let's make it:

  class MyPromise {
    constructor(callback) {
      this._thens = []
      this._catches = []
      callback(this.resolve.bind(this), this.reject.bind(this))
    }

    then(callback) {
          this._thens.push(callback)
      }

  catch(callback) {
    this._catches.push(catches)
  }

  resolve(value) {
    this._thens.forEach((then) => {
      then(value)
    })

    delete this._thens
  }

  reject() {
    this._catches.forEach((catcher) => {
      catcher(value)
    })

    delete this._catches
  }
}

const promise = new MyPromise((resolve) => {
  const ship = {
    name: "Time And Relative Dimension In Space",
  }

  setTimeout(() => {
    resolve(ship)
  }, 1000)
})

promise.then((ship) => {
  console.log(ship)
})
Try in REPL

async/await

The time before generators and promises was a dark one. A time full of callbacks, and callbacks, and more callbacks. It was the nightmare of every developer and aspiring actress.

Then promises came and things started to look up in our world. Light returned to the lands again. The grasses were growing, frameworks were being created in the villages again. The world looked brighter. And it has only gotten brighter. Generators finally gave us a way to process async code in a way that felt like synchronous code.

Generators aren't perfect though. Managing promises with generators involves a lot of boilerplate and libraries. So the elders of the internet came together and proposed a solution. The boilerplate should become a built in standard called "async/await".

Async functions would a be a special kind of generator and await would be a special kind of yield that would pause and yield the result of a promise to the generator. Async code could then be clear and simple.

It looks like this:

const getCats = async() => {
  const result = await fetch('/cats.json')
}

To handle errors you just need to use a try and catch block, the same way you would handle errors in synchronous code. It looks like this:

const getCats = async() => {
  try {
    const result = await fetch('/cats.json')
  } catch (err) {
    console.log(err)
  }
}

Async functions compile to a wrapped generator. The first generator yields promises up to a parent generator which handles yielding the results of those promises back to the child generator.

You now know async!