Working with async functions can be joyful and intriguing. The journey of the async programming can continue outside the scope of the running functions. It ensures a non-blocking form of IO programming structure. It’s commonly found in the event-based program interfaces and used in many different pieces in the web e.g. CRUD operations from the browser side or request call on the server side to a third party service. However, dealing with an increasing number of async functions in a program can be quite tedious if not being handled well.
Promises, as we know, can resolve or reject the result and pass the error if caught by the catch function. All of these add extra scopes to the already curated function to handle a desired result. Each of which can create a new callback function, depending on the workflow of the program, e.g. a script may ignore a certain error or be just logged into the log module.
In this article, we will take a look at four different ways to deal with promises, they all meet the exact same purpose but are different in terms of script maintainability, legibility and ease of setup. Finally, concluding with the easiest, efficient and effective approach.
I, personally, used them all in one point in time but started shifting towards the advanced and more maintainable approach to make my life easier while scripting and to give time to critically think about the overall solution, workflow, etc..
All examples will be running using Node.js for, and for the sake of demoing the results, Express.js module will be included to show the results as a web service call which might be part of real life scenario.
The following example is just for documentation and comparison purposes. Jump right to the end of the article if you don’t want to bother with these basic stuff.
#1 Callback (ES5)
The example is using ‘
https://jsonplaceholder.typicode.com’ as a service provider to generate a legit JSON object for the demo. Suppose that we only want to return a list of ids, they are passed as an array object to the HTTP response. As you can see the ‘ids’ are in a completely new scope which makes whatever is defined in there less handy if the we would like to build on the ids later.
#2 Promise.all (ES6)
Promise.all uses the global promise object. ‘All’ methods accepts an array of promises, think of it as a group of (db create & update) operations that run simultaneously but will resolve in different times. That method fires another promise object whenever all promises passed in the array are resolved/rejected. That’s definitely useful and better than the previous (callback) method, but is still creating new scope and isolating the results from the parental function. The next example will bring up a new resolution that maintains the running scope and shortens the written script for us.
#3 Async/Await (ES7)
This is part of the revolutionary newer Javascript specs. It uses promises to return the results and makes writing Javascript looks like synchronous scripting. Using this approach is a bit confusing for the ‘Promises’ users as it consists of two steps. Step 1: calling a function that returns a ‘promise’ object. Step 2 wrapping the function calling the promise with a ‘async’ function, that returns ‘AsyncFunction’ object. This approach uses a couple of reserved words ‘async’ and ‘await’ as shown in the example below:
Wrapping the scope with a async function is a must, that was a missing and confusing part for me for a while when the ‘async/await’ was first introduced. Await is where the magic happens, we strip the entire crappy callback and assign the response, when resolved, to the defined ‘const response = ..’ Fetching the JSON ‘users’ object and extracting their ids in the example were all done in a single scope and made clean and easy compared to the previous messy examples. That’s what we were aiming for at the beginning but let us look at the next approach.
#4 Generator Functions (ES6)
Generators are functions that can execute part of a function, exited and then re-entered. When married with promises, generators can be a very powerful tool to rely on when working with promises.
There is an npm module that simplifies working with generator functions named ‘co’. I personally think ‘co’ is a very powerful tool for a highly productive, efficient scripting and asynchronous interfaces. The following example gives a quick guide on how to use this module to extract the users JSON object again.
There are different ways in which generators can extract the response, but for brevity sake, we will only show a single way and will explore how that works.
The reserved word ‘yield’ is pretty much obvious, it is part of the generators implementation. Neat and clean? Yea. Why don’t we take a quick look on how that works by creating our version of ‘co’ and see how good is it?
Generators, Under the Hood
Let’s create our own generator function* rather than relying on a module.
The passed generator function, as an argument, is stored as an ‘IteratorFunction’ instance to be called later. It will be the global iterator that pushes the results out from the generator function. Once iterator is set, we need to reach the part that stores the ‘promise’ from the ‘Iteration’ object, and then resolve it to iterate over the resolved promise content again to bring the final result.
If we clean up this mess and factor our function, it may look something like the following:
Suppose that we’ve passed our lovely fetch as, a generator function, ‘(function* () { fetch(‘…’) })’, since that the argument is a function, executing it will not return the value immediately. Instead, it will return an iterator object that will be used to iterate over the values. Every time an iterator object is called, a promise is stored in the ‘value’ attribute. That promise will then need to be resolved to go to the next iteration. When the iterator function is finished, the flag ‘done’ is changed to ‘true’.
Conclusion
Eventually, it is the web developers call to stick with whatever he/she thinks is the most useful and beneficial for the written algorithm. I personally, think that generators are very powerful and the godfathers for the async/await implementation. Generators, when it was first released as a new es6 feature, it was like that person who packs his winter collection at the beginning of summer, ignored and thrown down the list for a greater focus on other useful specs.
I am sure there are many areas in which generator functions are of greatest benefits much like optimizing promises and callbacks. If you know any of these, you can list them in a comment and share the love.
Here is a li