Why I Don’t Use Async Await

A lot of JavaScript developers speak in exceptions. However, JavaScript does not have any defined practices on “good exception handling”. What does good mean? All using try/catch, .catch for Promises, and window.onerror in the browser or process.on for Node.js? Just http/file reading/writing calls? 3rd party/vendor systems? Code with known technical debt? None “because fast, dynamic language”?

In my view, good exception handling is no exceptions. This means both writing code to not throw Exceptions, nor cause them, and ensuring all exceptions are handled.

However, that’s nearly impossible in JavaScript as it is a dynamic language and without types, the language encourages the accidental creation of null pointers. You can adapt certain practices to prevent this.

One in the particular is not using async await.

A warning, this is a minority view, and only some functional languages hold this view. I also acknowledge my Functional Programming bias here. JavaScript accepts all types of coding styles, not just FP.

The Promise

Promises are great for a variety of reasons; here are 4:

  1. They have built-in exception handling. You can write dangerous code, and if an Exception occurs, it’ll catch it, and you can write a catch function on the promise to handle it.
  2. They are composable. In functional programming, you create pure functions, which are rad by themselves, and you wire them together into pipelines. This is how you do abstraction and create programs from functions.
  3. They accept both values and Promises. Whatever you return from the then, the Promise will put into the next then; this includes values or Promises, making them very flexible to compose together without worry about what types are coming out.
  4. You optionally define error handling in 1 place, a catch method at the end.
const fetchUser => firstName => 
  someHttpCall()
  .then( response => response.json() )
  .then( json => {
    const customers = json?.data?.customers ?? []
    return customers.filter( c => c.firstName === 'Jesse' )
  })
  .then( fetchUserDetails )
  .catch( error => console.log("http failed:", error) )

However, they’re hard. Most programmers do not think in mathematical pipelines. Most (currently) think in imperative style.

Async Await

The async and await keywords were created to make Promises easier. You can imperative style code for asynchronous operations. Rewriting the above:

async function fetchUser(firstName) {
  const response = await someHttpCall()
  const json = await response.json()
  const customers = json?.data?.customers ?? []
  const user = customers.filter( c => c.firstName === 'Jesse' )
  const details = await fetchUserDetails(user)
  return details
}

But there is a problem, there is no error handling. Let’s rewrite it with a try/catch:

async function fetchUser(firstName) {
  try {
    const response = await someHttpCall()
    const json = await response.json()
    const customers = json?.data?.customers ?? []
    const user = customers.filter( c => c.firstName === 'Jesse' )
    const details = await fetchUserDetails(user)
    return details
  } catch(error) {
    console.log("error:", error)
  }
}

However, there are also some nuances. For example, we want to separate the error handling for someHttpCall and it’s data handling from fetchUserDetails.

async function fetchUser(firstName) {
  try {
    const response = await someHttpCall()
    const json = await response.json()
    const customers = json?.data?.customers ?? []
    const user = customers.filter( c => c.firstName === 'Jesse' )
    try {
      const details = await fetchUserDetails(user)
      return details
    } catch(fetchUserDetailsError) {
      console.log("fetching user details failed, user:", user, "error:", fetchUserDetailsError)
    }
  } catch(error) {
    console.log("error:", error)
  }
}

This can get more nuanced. Now you have the same problem you have with nested if statements, it’s just quite hard to read. Some don’t view that as a problem.

Golang / Lua Style Error Handling

The Golang and Lua devs do view that as a problem. Instead of Exception handling like JavaScript/Python/Java/Ruby do, they changed it to returning multiple values from functions. Using this capability, they formed a convention of returning the error first and the data second. This means you can write imperative code, but no longer care about try/catch because your errors are values now. You do this by writing promises that never fail. We’ll return Array’s as it’s easier to give the variables whatever name you want. If you use Object, you’ll end up using const or let with the same name which can get confusing.

If you use traditional promises, it’d look like this:

const someHttpCall = () =>
  Promise.resolve(httpCall())
  .then( data => ([ undefined, data ]) )
  .catch( error => Promise.resolve([ error?.message, undefined ]) )

If you are using async await, it’d look like this:

function someHttpCall() {
  try {
    const data = await httpCall()
    return [ undefined, data ]
  } catch(error) {
    return [ error?.message ] 
  }
} 

If you do that to all your async functions, then when using your code, it now looks like this:

async function fetchUser(firstName) {
  let err, response, json, details
  [err, response] = await someHttpCall()
  if(err) {
    return [err]
  }

  [err, json] = await response.json()
  if(err) {
    return [err]
  }

  const customers = json?.data?.customers ?? []
  const user = customers.filter( c => c.firstName === 'Jesse' );
  [err, details] = await fetchUserDetails(user[0]);
  if(err) {
    return [err]
  }
  
  return [undefined, details]
}

Then if all your functions look like this, there are no exceptions, and all functions agree to follow the same convention. This has some readability advantages and error handling advantages elaborated on elsewhere. Suffice to say, each line stops immediately without causing more errors, and secondly, the code reads extremely imperative from top to bottom which is preferable for some programmers.

The only issue here is not all errors are handled despite it looking like it. If you mispell something such as jsn instead of json or if you forget to wrap a function in this style like response.json, or just generally miss an exception, this style can only help you so much.

Additionally, you have to write a lot more code to put the error first, data last. The worse thing about this style is the constant checking if(err). You have to manually do that each time you call a function that could fail. This violates DRY pretty obnoxiously.

Conclusions

You know what doesn’t violate DRY, isn’t verbose, and handles all edge cases for exceptions, only requiring you to put exception handling in one place, but still remains composable?

Promises.

const fetchUser => firstName => 
  someHttpCall()
  .then( response => response.json() )
  .then( json => {
    const customers = json?.data?.customers ?? []
    return customers.filter( c => c.firstName === 'Jesse' )
  })
  .then( fetchUserDetails )
  .catch( error => console.log("http failed:", error) )

4 Replies to “Why I Don’t Use Async Await”

  1. Strange, the single catch at the end of a promise chain you consider a plus point, even though it’s virtually impossible to debug where it came from. Whereas the single try/catch clause you consider a minus point. IT’S EXACTLY THE SAME THING.

    You can have interleave different then/catches exactly as you can interleave different try/catches.


    someFn()
    .then(anotherFn)
    .catch(thatFn)
    .then(anotherFn)
    .then(anotherFn)
    .catch(thatFn)

    async wrapper() => {
    try {
    await someFn();
    } catch(e) {
    thatFn()
    }
    try {
    ...
    etc

    I really see no difference, except that you can debug the async/await much more easily.

    Another thing that you can do, of course, is to create different types of errors, and catch them all at the end


    try {
    await someFn()
    if (something) {
    throw ErrorX()
    }
    await anotherFn() || throw ErrorY();
    ...
    }
    catch (e) {
    if (e instaceof ErrorX)
    ...

    1. Apologies about the formatting, blogs are the worst at that.

      The point isn’t about “where it came from”. I get with try/catch it’s “it’s this try/catch block _here_ that is the problem”. That’s a start, sure, but generally at runtime, none of it matters. What you should be doing is:

      Step 1: promise chain to catch all or try/catch all.
      Step 2: return errors from functions vs. raising & explaining them vs. “where did things go wrong”. Like Go/Lua.
      Step 3: If more than 1 possible failure, return union type / discriminated union.
      Step 4: If you continue to use JavaScript without types, still keep 1 single catch or try/catch.

      Step 2 and 3 aren’t what people usually think about. In JavaScript, the attitude typically is “anything can fail, keep running as fast as possible the code over and over until we figure out all possible errors”. Instead, if you focus on an individual function’s ability to either work or not. Pure functions like “getting todays date”, you’re ok, but things like “make an HTTP call” are insanely complex. At first, you can only return 2 things; it worked, or didn’t. Later, once you learn the HTTP status codes, what their values mean with the API you’re interacting with, and the response JSON/XML/HTML/whatever format, you can further add more helpful return values or error values.

      In short, don’t use throw, don’t use catch. Make all functions pure, and only return errors at first that verbose. Later, you can return multiple types of errors, and either chain them using Promises (since catch is built-in) or their native bind functionality.

Comments are closed.