Traditional ways of writing asynchronous code
Before the introduction of async/await, the most common pattern of writing asynchronous code was using callbacks. ES2015 introduced us with Promises which tried to solve some of the issues of callback functions like “Inversion of control” and “Callback hell” but at the end of the day, it was always a challenge managing complex nested level asynchronous code.
Most of the problems with callbacks and Promises are due to the way the code looks. It won’t be wrong to say that in both the cases, the code looks in a way, unnatural to our brain and how we think.
With a complex looking code, it usually brings a whole lot of other issues with it. It becomes harder to test the code, is easier to introduce bugs and can be a nightmare for someone else to maintain the code. It can obviously be cleaned up using reusable functions or multiple then chains but still you get the point that I am trying to make here. We needed a synchronous looking asynchronous code. Not just to make it look better but also to remove the other complexities that this pattern of code brought.
On comes generator functions
ES2015 introduced generator functions. Generators are basically functions that can be paused and resumed. According to this MDN document,
Generators are functions that can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.
In the above example, calling generator(10) just returns an iterator and yield i statement is never called unless we call gen.next() which returns us the value that is being yielded. In short, the generator function doesn’t execute the entire function but waits for iterator.next() to be called and again pauses at the next yield and the cycle continues.
Now combine this with Promise and a recursive function, we have the recipe for our synchronous looking asynchronous code.
The run function takes in a generator function. We recursively iterate over the iterator calling the iterator.next function on Promise resolve until the generator has no more yield keywords before finally exiting the recursive function.
This gave developers an option to take the asynchronicity out of the equation and abstract it away and just write (and also read) code that makes sense. Packages like co and coroutine became very popular with developers which took the advantage of asynchronous generator function wrappers. Transpilers like Babel also made use of asynchronous generator functions to allow using async/await keywords. This became a stepping stone for ES7 async/await.
This gets converted to the code below in V8's native implementation.
With async/await, writing async keyword before a function automatically returns us a promise and await keyword waits for the Promise to resolve before executing the lines next to it. We are not really creating a synchronous blocking code but just writing code in a way that just looks synchronous taking away the nested level code blocks.