The ease and benefits of Async/Await
There are a handful of ways to play to the strengths of asynchronous function calls and properly handle their execution, but one is far superior to the rest (spoiler: it’s Async/Await). In this quick read, you’ll learn about the ins and outs of Promises and the use of Async/Await, as well as our opinion on how the two compare.
Promises vs. Callbacks
There are small but important differences between the two. At the core of every Promise, there is a callback resolving some kind of data (or error) that bubbles up to the Promise being invoked.
The callback handler:
The code snippet below shows a full end to end check for validating a password (it’s static and must match “bambi,” my favorite cartoon character as a child):
The code is commented pretty well. However, if you’re confused, the catch only executes in the event that a
reject() is called from the Promise. Since the passwords don’t match, we call
reject(), therefore “catching” the error and sending it to the
Promises provide a simpler alternative for executing, composing, and managing asynchronous operations when compared to traditional callback-based approaches. They also allow you to handle asynchronous errors using approaches that are similar to synchronous try/catch.
Promises also provide three unique states:
- Pending — the Promise’s outcome hasn’t yet been determined because the asynchronous operation that will produce its result hasn’t completed yet.
- Fulfilled — the asynchronous operation has completed, and the Promise has a value.
- Rejected — the asynchronous operation failed, and the Promise will never be fulfilled. In the rejected state, a Promise has a reason that indicates why the operation failed.
When a Promise is pending, it can transition to the fulfilled or rejected state. Once a Promise is fulfilled or rejected, however, it will never transition to any other state, and its value or failure reason will not change.
The one thing Promises don’t do is solve what is called “callback hell,” which is really just a series of nested function calls. Sure, for one call it’s OK. For many calls, your code becomes difficult, if not impossible, to read and maintain.
Looping in Promises
The correct way to approach this type of situation is to use
Promise.all(). This function waits for all fulfillments (or the first rejection) before it is marked as finished.
Error handling with multiple nested Promise calls is like driving a car blindfolded. Good luck finding out which Promise threw the error. Your best bet is to remove the
catch() method altogether and opt in for a global error handler (and cross your fingers) like so:
Note: The above two options are the only two ways to ensure that you’re catching errors. If you miss adding a
catch()method, it’ll be swallowed up by the code.
Callback hell is a term used to describe the following scenario:
Note: As an example, here’s an API call that would get four specific users from an array.
Whew, that’s ugly and takes up a TON of space in the code.
Note: Here’s an example of the same set of API calls to retrieve four users from an array, in more than half the lines of code:
And because Async/Await is built on top of Promises, you can even use
Promise.all() with the await keyword:
Note: Async/Await is slightly slower due to its synchronous nature. You should be careful when using it multiple times in a row as the Await keyword stops the execution of all the code after it — exactly as it would be in synchronous code.
How Do I Start Using Async/Await?
Working with Async/Await is surprisingly easy to understand and use. In fact, it’s available natively in the latest version of Node.js and is quickly making its way to browsers. For now, if you want to use it client-side, you’ll need to use Babel, an easy-to-use and setup transpiler for the web.
Let’s start with the Async keyword. It can be placed before function, like this:
Why Is Async/Await Better?
Now that we’ve gone over a lot of what Promises and Async/Await have to offer, let’s recap why we (Stream) feel that Async/Await is a superior choice for our code base.
- Async/Await allows for a clean and concise code base with fewer lines of code, less typing, and fewer errors. Ultimately, it makes complicated, nested code readable again.
- Error handling with try/catch (in one place, rather than in every call)
- Error stacks make sense, as opposed to the ambiguous ones that you receive from Promises, which are large and make it difficult to locate where the error originated. Best of all, the error points to the function from which the error came.
It took less than one day to understand the syntax and see what a mess our code base was in that regard. It took about two days total to convert all of our Promise-based code to Async/Await, essentially a complete rewrite — which just goes to show how little code is required when using Async/Await.
Lastly, thank you for reading this piece. If you enjoyed it, please give me a clap (or 50 if you’re feeling extra nice). Thank you!