Promises solve a lot of issues that callbacks trip over, helping to clean up readability by avoiding callback hell and endless callback jumps. They also empower us with more control over asynchronous code, providing .then and .catch blocks. Even so, there are still times when these wonderful little tools falter.
Have you ever tried implementing a series of complex conditionals inside a Promise chain? It becomes quite ugly. You might end up with multiple nested promises, perhaps even six, or seven layers deep. Sprinkle in a few if and else statements, and you’ve got a recipe for disaster.
You could flatten those nested Promises so that each .then block returns a new Promise. Even then, you’d be dealing with a large number of chained Promises. Even then, it’d still be messy to keep track of return values on the tenth Promise block. What if you had to keep track of every value returned in every single one of the blocks?
One option would be an object that is passed through each .then block, gathering information along the way. Another option would be a global object that every block has access to. Both are acceptable, but come with their own downfalls that may eventually trigger bugs that are extremely hard to squash. Globals can easily be accessed and changed, while passed objects can become tricky to track in deeply nested blocks.
Thankfully, ES2017 comes with a few tricks up its sleeves. Async functions are a feature currently implemented in Node 8.x, and help alleviate a lot of the problems encountered when one solely relies on Promises and/or callbacks to handle all the heavy lifting. What’s even cooler is the fact that an await function returns a promise itself!
So what exactly is an async function? It is syntactic sugar built off of the backs of generators and promises (quite interesting to see an await function’s behind-the-scenes). An in-depth explanation is beyond the scope of this post, but simply put, it is another way of writing asynchronous code. It gives the illusion of code that looks and behaves in a manner similar to synchronous code, and provides a cleaner interface for doing so.
Here is how the general syntax looks.
It is important to note that you can only utilize .await if it’s in the same function scope as the async function. If you declared an async function with another function inside, and tried using .await in the inner function, you would get a syntax error. It’s fairly easy to stumble upon this error, so be aware.
This implementation is not a cure-all for every problem, and in some cases, you are better off utilizing Promises (concurrency). However, .await works by casting the function into a Promise – meaning anything returned from an await function can also be chained with a .then as well!
The difference between the two examples above is night and day. It is easier to read, understand, and with a small modification – debug as well. In order to handle error cases, simply wrap the contents of the async function in a try/catch block. The catch block will also send any errors encountered inside of the await function to your error handlers. By doing so, you are taking of synchronous and asynchronous code simultaneously.
It is important to understand that while async functions are a great tool to have, it may turn your code into a series of singular await functions that need to wait on each other in order to execute. Serial code. This means that the most efficient way to write async functions needing to execute in parallel is to utilize Promise methods like .all, resolving all your await functions at once.
Remember, it is important to know when your code could be running concurrently when they are currently running sequentially. Async functions are yet another set of tools that can be mixed and matched with other asynchronous coding methods, and a good understanding of generators and promises will yield greater results in one’s ability to utilize async functions.