Oscar Funes

Published on

Thoughts about Async/Await

I’m guessing that by now Medium and the internet are full of posts around this new feature added to JS and recently landed on Node LTS Carbon. Since I’m starting to use it now at work, beyond experiments. I’ll explain how I understand this feature and how I see it being implemented, with some questions I have. First, let’s start with promises, a promise is an object that represents an ongoing asynchronous operation. Like reading a file, or the usual asynchronous things that callbacks handle.

function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if (err) return reject(err)
      return resolve(data)
    })
  })
}

Now Node ships with a util.promisify that understands node’s callback style of error first and returns a function that expects the same arguments sans the callback and returns a promise.

How you handle this? Well, a promise accepts 2 handlers, one for the resolve value and one for the reject error (or reason).

const p = readFilePromise('path/to/file')

p.then((data) => console.log(data)).catch((err) => console.error(err))

// or not referencing the Promise at all
readFilePromise('path/to/file')
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

How does async/await enters into play? Well, you await for promises, and declaring an async function implicitly returns a promise With the caveat that you can only use await inside an async function

async function readMeAFile() {
  const path = './some/path'
  const data = await readFilePromise(path)
  console.log(data)
}

But what about errors? Well, before async/await , there were different ways to propagate errors to our users, either by throwing err or by the callback err first, or by rejecting a promise with an error. Now the throw and the asynchronous errors are merged as just one, and can be intercepted through the almost forgotten :P try-catch block.

async function readMeAFile() {
  try {
    const path = './some/path'
    const data = await readFilePromise(path)
    console.log(data)
  } catch (err) {
    console.error(err)
  }
}

How to differentiate between errors?

Currently, the catch doesn’t support to be filtered like Bluebird catch, or like Java, etc. I’ve recently investigated about this and found the bounce module by Eran Hammer for the hapi ecosystem. I like this module because it allows to filter system errors and boom errors (which is something specific to hapi). But not everyone uses or creates modules that throw boom errors. So you’re still on your own in regards of filtering and responding to non system errors. From bounce’s README

try {
  await email(user)
} catch (err) {
  Bounce.rethrow(err, 'system') // Rethrows system errors and ignores application errors
}

How to not try catch everywhere?

I think I will approach this, by not using async functions everywhere. Just where I combine multiple features, basically on the last “layer” of my application, at controller level mostly. In other places I will just keep using promises and their handlers explicitly. Or just callbacks, synchronous execution. Just the right tool for the problem. Adding async to everything will wrap all my code in promises which always resolve on the next tick, which can create an (sometimes) unnecessary delay on my code.

How to use middlewares on express?

Since Hapi v17 now uses async/await internally, there’s no need to mention it. And I mostly use express on a daily basis. From looking around, creating a wrapper for async middlewares seems the easiest.

const wrapper = (mw) =>
  (req, res, next) => {
    Promise.resolve(mw(req, res, next))
     .catch(next);
  };

app = express();

app.use(‘/route’, wrapper(async mw(req, res, next) {
  // if this throws a type error it will be catched
  // by the wrapper and sent to next(err)
  const val = req.some.deep.prop;
  // if this throws it will also be catched
  // by the wrapper and sent to next(err);
  const payload = await someData(val);
  res.stats(200).json(payload);
}));

You still have to understand there’s promises underneath and what they’re all about

This is not really a question but more of a caveat, from my perspective. Let’s look at an example:

async function doSomething() {
  const x = await getX()
  const y = await getY()
  return something(x, y)
}

Even though this code handle promises through the use of await, and each time you await you return control to the event loop, you’re still executing one promise after another for 2 results that don’t really depend on each other. Currently, there are some approaches for parallel execution, which involve knowing about the underlying promises.

// Using Promise.all()
async function doSomething() {
  const [x, y] = await Promise.all([getX(), getY()])
  return something(x, y)
}

// Creating the promises and awaiting
async function doSomething() {
  const xPromise = getX()
  const yPromise = getY()
  const [x, y] = [await xPromise, await yPromise]
}

Async / Await is great for testing

I think where it shines the most is with testing as shown in this example from AVA. I think it becomes more readable and easier to reason about your promises this way. The next example was taken from AVA’s README.

// Async arrow function
test(async (t) => {
  const value = await promiseFn()
  t.true(value)
})

Conclusion

Async/Await is definitely an interesting syntactic sugar for promises and how we interact, read and reason about them. I think there’s still some things to find out when migrating the existing code base to using this feature. I’ll try to take my first approach to using them through my current suite of unit tests. How are you handling async await? Are you even using it? And if so, how deep in your code are you using it?