Is Async / await a step backward for JavaScript?

Original author: Gabriel Montes
  • Transfer


At the end of 2015, I heard about this pair of keywords that broke into the JavaScript world to save us from the promise chain hell, which, in turn, was supposed to save us from the callback hell. Let's see a few examples to understand 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 an external service
- change and write data back to the database

Also, let's assume that we don’t have any Any knowledge about 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. You are now familiar with him. 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 clear the code a bit. The code would get better, since error handling would at least 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 was no longer angry with us; we felt that we needed 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! However, there was too much overhead in the form of a lot of then () calls, function () {...} blocks, and the need to add multiple return statements everywhere.

Finally, we hear about ES6, about all these new things that came in JavaScript: like arrow functions (and a bit of destructuring to make it a little more fun). We decide to give our beautiful code one more 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 mutate the data one by one, which we collect using various asynchronous operations. We did not define intermediate variables for storing this state, and error handling is in one understandable place. Now we are sure that we have definitely reached JavaScript heights! Or not yet?

And async / await comes


A few months later, async / await enters the scene. He was going to get into the ES7 specification, then the idea was postponed, but because there is babel, we jumped on the train. We learned that we can mark a function as asynchronous and that this keyword will allow us to “stop” its execution flow inside the function until the promise decides that our code looks synchronous again. In addition, the async function will always return a promise, and we can use try / catch blocks to handle errors.

Not too confident in 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 regular imperative synchronous code. Life went on 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 more recently, the paradigm has begun to gain momentum. And only recently have we begun to understand the benefits of a functional approach.

We begin teaching some of its principles. We learn new words, such as functors, monads, monoids - and suddenly our dev-friends begin to consider us cool, because we use these strange words quite often!

We continue our voyage into the sea of ​​a 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 benefits of immutability so as not to store or mutate the state, to create complex logic by combining simple functions, to avoid loop control and all the magic to be 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 features.

But ... wait!


We have seen all these functional models in the past. We remember how we used promises and how we combined functional transformations one by one without the need to manage state or branch our code or manage errors in an imperative style. We already used the promise monad in the past with all the attendant benefits, but at that time we simply did not know the word!

And we suddenly understand why async / await-based code looked weird. After all, we wrote the usual imperative code, as in the 80s; handled errors with try / catch, as in the 90s; controlled the internal state and variables by doing asynchronous operations using code that looks like synchronous, but that stops suddenly and then automatically resumes when the asynchronous operation is completed (cognitive dissonance?).

Last thoughts


Do not get me wrong, async / await is not the source of all evil in the world. I actually learned to love it after months of use. If you feel comfortable writing compulsory code, learning how to use async / await to control 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 .

Also popular now: