
At the end of 2015, I heard about this pair of keywords that broke into the world of JavaScript to save us from the promise chain hell, which, in turn, was supposed to save us from the callback hell. Let's look at a few examples to see how we got to async / await.
Suppose we are working on our API and must respond to requests with a series of asynchronous operations:
- check the validity of the user
- collect data from the database
- get data from external service
- change and write data back to the database
')
Also, let's assume that we do not have any knowledge of promises, because we travel back in time and use callback functions to process the request. The solution would look something like this:
function handleRequestCallbacks(req, res) { var user = req.user isUserValid(user, function (err) { if (err) { res.error('An error ocurred!') return } getUserData(user, function (err, data) { if (err) { res.error('An error ocurred!') return } getRate('service', function (err, rate) { if (err) { res.error('An error ocurred!') return } const newData = updateData(data, rate) updateUserData(user, newData, function (err, savedData) { if (err) { res.error('An error ocurred!') return } res.send(savedData) }) }) }) }) }
And this is the so-called callback hell. Now you are familiar with it. Everyone hates him because it is difficult to read, debug, change; it goes deeper and deeper into nesting, error handling is repeated at each level, etc.
We could use the famous
async library to clean up the code a bit. The code would be better, since error handling, at least, would be in one place:
function handleRequestAsync(req, res) { var user = req.user async.waterfall([ async.apply(isUserValid, user), async.apply(async.parallel, { data: async.apply(getUserData, user), rate: async.apply(getRate, 'service') }), function (results, callback) { const newData = updateData(results.data, results.rate) updateUserData(user, newData, callback) } ], function (err, data) { if (err) { res.error('An error ocurred!') return } res.send(data) }) }
Later we learned how to use promises and thought that the world is no longer angry with us; we felt that we need to refactor the code again, because more and more libraries are also moving into the world of promises.
function handleRequestPromises(req, res) { var user = req.user isUserValidAsync(user).then(function () { return Promise.all([ getUserDataAsync(user), getRateAsync('service') ]) }).then(function (results) { const newData = updateData(results[0], results[1]) return updateUserDataAsync(user, newData) }).then(function (data) { res.send(data) }).catch(function () { res.error('An error ocurred!') }) }
It is much better than before, much shorter and much cleaner! Nevertheless, there was too much overhead in the form of a set of then () calls, function () {...} blocks and the need to add several return statements everywhere.
And finally, we hear about ES6, about all these new things that have come to JavaScript: for example, pointer functions (and a little destructuring, so that it is a little more fun). We decide to give our beautiful code another chance.
function handleRequestArrows(req, res) { const { user } = req isUserValidAsync(user) .then(() => Promise.all([getUserDataAsync(user), getRateAsync('service')])) .then(([data, rate]) => updateUserDataAsync(user, updateData(data, rate))) .then(data => res.send(data)) .catch(() => res.error('An error ocurred!')) }
And here it is! This request handler has become clean, easy to read. We understand that it is easy to change if we need to add, delete, or swap something in the stream! We have formed a chain of functions that one after another mutate the data that we collect through various asynchronous operations. We did not define intermediate variables for storing this state, and error handling is in one clear place. Now we are sure that we have definitely reached JavaScript heights! Or not yet?
And comes async / await
A few months later, async / await goes on stage. He was going to get into the specification of ES7, then the idea was postponed, but because there is Babel, we jumped into the train. We learned that we can mark a function as asynchronous and that this keyword will allow us inside the function to “stop” its execution thread until promise decides that our code again looks synchronous. In addition, the async function will always return a promise, and we can use try / catch blocks to handle errors.
Not too sure about the benefits, we give our code a new chance and go for a final reorganization.
async function asyncHandleRequest(req, res) { try { const { user } = req await isUserValidAsync(user) const [data, rate] = await Promise.all([getUserDataAsync(user), getRateAsync('service')]) const savedData = await updateUserDataAsync(user, updateData(data, rate)) res.send(savedData) } catch (err) { res.error('An error ocurred!') } }
And now the code again looks like the old usual imperative synchronous code. Life continued as usual, but something deep in your head tells us that something is wrong here ...
Functional programming paradigm
Although functional programming has been around us for more than 40 years, it seems that quite recently the paradigm began to gain momentum. And only recently we began to understand the advantages of the functional approach.
We begin learning some of its principles. We learn new words, such as functors, monads, monoids - and suddenly our dev-friends start to consider us cool, because we use these strange words quite often!
We continue our voyage in the sea of the functional programming paradigm and begin to see its real value. These proponents of functional programming were not just crazy. They were probably right!
We understand the advantages of immutability in order not to store and not mutate a state, to create complex logic by combining simple functions, to avoid loop control, and so that all the magic is done by the language interpreter itself, so that we can focus on what is really important to avoid branching. and error handling by simply combining more functions.
But ... wait!
We have seen all these functional models in the past. We remember how we used promises and how we connected functional transformations one by one without having to manage the state or branch of our code or manage the errors in an imperative style. We have already used the promise monad in the past with all the attendant benefits, but at that time we simply did not know this word!
And we suddenly understand why the async / await code looked weird. After all, we wrote the usual imperative code, as in the 80s; processed errors with try / catch, as in the 90s; managed the internal state and variables, doing asynchronous operations using a code that looks like synchronous, but which stops suddenly and then automatically continues execution when the asynchronous operation is completed (cognitive dissonance?).
Last thoughts
Don't get me wrong, async / await is not the source of all evil in the world. I actually learned to love it after several months of use. If you feel comfortable when writing imperative code, learning how to use async / await to manage asynchronous operations can be a good move.
But if you like promises and want to learn how to apply more and more functional programming principles, you can just skip async / await, stop thinking imperatively and move on to the new-old functional paradigm.
see also
→
Another opinion that async / await is not such a good thing .