Async Await Saves The Day (Sort Of)

Leigh Steiner
codeburst
Published in
8 min readFeb 8, 2018

--

No human is an island, and no chunk of code, either. Async/Await is awesome, but it isn’t going to replace JavaScript promises in our asynchronous world. Async/Await relies heavily on promises, so if you are shaky in what they are or how to use them, start by checking out my Sixteen Minute Guide to JavaScript Promises. (We’ll do a very short review here, but it’s more to set the stage than anything else.)

So, on we go. You’ve probably heard great things about async await and how it’s going to revolutionize your code and whiten your teeth and clear your skin. Let’s do a teensy-tiny bit of set up:

Why do we need Promises?

We need promises to handle asynchronous actions in JavaScript. JavaScript happens mostly in a non-blocking way — we kick off an asynchronous action, and then move on with our lives and our code, without waiting see when or even if it finishes. This is great, as far as our end products go — if JavaScript could only run synchronous, blocking code, the internet as we know it wouldn’t exist. Every time we did something like using fetch or axios, anything that happened “slowly” by computer standards, our entire browser page would effectively lock until each thing finished. Instead, our event loop manages the tasks that need to happen after an async action completes, and magically, our expressions don’t need to wait in long lines for things to finish that don’t matter to them. This is great for our end user, who gets shiny, smooth interactive experiences, but suddenly is kinda crappy for the developer. There are a lot of things that we ultimately want to make wait because they rely on information that we haven’t finished getting yet. That’s where promises come in — they give us an imaginarily-synchronous pipeline where we can deal with the data once it arrives, while still giving us a lot of the freedom of multi-threaded program execution.

So why aren’t they good enough?

Before we had promises, we used vanilla async callbacks. And while, functionally, callbacks gave us a lot of the same abilities, they were nightmarish to look at, debug and maintain (we called it “callback hell” for a very good reason). Promises are a lot better, but all of those thens and catches that actually take callbacks are still a lot to keep track of, and they still limit the flow of our data in some really obnoxious ways. Let’s look at an example:

Imaginary Promises

We have a problem here. We can’t really use the result of somePromise after we go to get the reliantNewPromise. Theoretically, we could do them both at the same time using Promise.all(), but it looks like we need the result of somePromise in order to actually structure our reliantNewPromise request. As it stands, we need to contain everything we want to do with each piece of data inside of the the single .then() which receives it.

How does Async Await make it all better?

Async Await gets rid of chaining for us (well, almost. more on that later). It turns our async function into a peaceful, magical bubble of scope where we can trigger asynchronous actions that, within this sorcerous realm, obey synchronous rules, while still not blocking anything that goes on outside of that function.

Implementation

Implementation is very, very straightforward. Pretty much any sort of function can use async/await — all you have to do is preface the function with the word “async,” and you’re in business. In async-ed functions (as opposed to simply asynchronous functions), you have the ability to place the “await” keyword in front of any expression that returns a promise. Your function will pause at those labeled points and await that promise’s fulfillment before moving on with the rest of the function. Synchronous code wrapped in a sea of non-blocking goodness! The only catch is this — like Cinderella’s coach, our data will turn right back into pumpkins outside of our async-ed function scope. In fact (and this will be important in a minute, I promise), a function that is async-ed returns a promise that itself will resolve with the return value.

(This is why I said that you have to have a pretty good grasp of promises to use async/await — they are here to stay.) Let’s check it out:

a function that returns some async express middleware, which is a great use case

So, you can see in this example, we’re going to return some express middleware that modifies our request body going forward. We make simple, async calls to our DB that we want to run consecutively, rather than concurrently, because we can’t go looking for the right docs without the necessary user information already in place — this means that Promise.all() would be no help to us here.

The fantastic thing is this: we await each DB call individually, but when they’ve finished, we still have access to both the full user and all of the docs at the same time. We can just assign them to variables, as we’ve done here, and then push those variables around anyway we might push normal, non-promise-y data around. We can mutate them, compare them, put them into other objects, whatever we need. Things that would be just about impossible with a promise chain. Within the bounds of that async function, we can treat these variables just like any other type of data.

Async/Await is powerful, simple to use, and a great way to shape code flow in a readable, human way. Looking at the snippet above, we can easily intuit what’s going on, and confidently assess what will happen when.

Pretty fantastic, right? But what about…when things go wrong?

Error Handling in Async/Await

We’re going to spend more time on this than you might expect. Because implementing async/await is really just that easy — there’s not a ton of obscure methods or tricky use cases: async-ify a function, await a promise inside of it, boom.

In some ways, error handling is the most important part of the code you write — if we want people to rely on our applications, then we need them to not be impressed with the smooth functioning of our application. We need them to take it for granted. And that means having good safety nets in place so that, when some promise just doesn’t play us true, our app still runs the way we need it to run.

There are four pretty straightforward ways to manage errors in async-ed functions.

  1. Try/Catch Blocks:
our middleware generator, refactored to handle errors

The simplest way to handle errors with async/await is simply to wrap your relevant code in a try block, and catch the errors beneath it. Functionally, this is effective, but some developers find it messy and unintuitive to follow.

2. The Error Handler Factory (Higher Order Functions):

build it once, use it over and over again

An await-ed expression is still essentially a promise, underneath. Just a promise with ‘extras.’ So what can we do with promises? We can chain .catch()’s off of them, right? One solution to our need for error handling is to write a higher order function that takes our awaited action as a function and returns a new function that is essentially the same function with a .catch() appended with our error handling inside of it. This is a slick, modular way to handle lots of errors with very DRY code.

look at hot-shot over here, doing it all in one line

3. Catch It In The Moment:

So, if we can make a HOF to automatically make our functions error handling by chaining .catch()’s on them, it stands to reason that we can just…chain a .catch() after a given async-ed action. This is a good solution for moments when you expect to encounter unique errors that require special handling.

4. Handle The Unhandle-able:

very global error handling

Fun fact — Node.js wants to help us and make everything better. Every time we do something promise-y without an attached error handler, an event is emitted called ‘unhandledRejection.’ So one way to be sure that you never miss an error is to write the error handling straight on to your process — it can listen directly for those events and fire the appropriate callback when it encounters them. What’s fun about this is that it’s a great catch-all (get it?) — any more specific handlers that you attach elsewhere will essentially “overwrite” this one, as they will prevent an unhandledRejection event from emitting.

The Limitations of Async/Await

Alas, async/await is not perfect, or at least, does not solve every possible synchronicity related issue that we might have perfectly. As we talked about earlier, async/await’s powers are limited — once you cross the border of an async-ed function’s scope, you’re back into quotidian, chaining promise-land. You can’t, unfortunately, await a promise inside a function and return its value as raw data.

Furthermore, async/await is not always a preferable option over straight up promises (compared to how, once we have promises, we pretty much should never be bothering with vanilla async callbacks ever again). Promises have the potential to be more efficient than awaiting, depending on our specific needs. We can only await one thing at time, no matter what — there is no await.all(). If we want to make asynchronous requests that fire concurrently, async/await is not a good solution. It’s not all bad news though — because async/await operates directly on promises, there’s nothing stopping you from mixing the two tools in your codebase. One great approach is to use async/await as a default, in the name of clean, easy to follow code, and to revert to promises when and only the needs of your application call for their extra flexibility.

--

--

Full Stack Developer. Currently telling stories with data @ Locus Analytics. Former Teaching Fellow @ Grace Hopper. Bug me to blog about D3. pronouns: she/they