DEV Community

Cover image for Why to use async & await instead of the Promise class?
Svetloslav Novoselski
Svetloslav Novoselski

Posted on • Originally published at blog.novoselski.net

Why to use async & await instead of the Promise class?

In order to answer this question we should first say what are actually promises and what they do.

What are promises

Promises are a way to write asynchronous code, which means that a function will return some data in a certain time in the future and the code will continue to execute. For example let's say we want to make a burger because we are hungry. So what we do, we can put the meat on the stove, and in parallel with this we can cut the vegetables. When the meat is ready we remove it from the hot plate and put it in the burger, then we put the vegetables and we are ready. Cooking the meat is the Promise and when it is ready we do something with it, this is what we call asynchronous programming.

Usage

Here a simple function that get some data load some data. These are the two ways to do it with the Promise class which has 2 methods .then when the promise has ended and .catch when there is an error.

const makeRequest = () => {
    getUserData
        .then(user => {
            console.log(user);
        });
}
const makeRequest = async () => {
   console.log(await getUserData());
}

What we see is that using async & await we write less code but in this way when there is 1 function it doesn't matter if you use await or promise class. But what will happen if there are 2 or more functions that depend on each other. For example, first we want to get a user and then we want to get all clubs that a user has joined. Here are the 2 implementations.

const makeRequest = () => {
    getUserData
        .then(user => {
            getClubsForUser(user.id)
                .then(clubs => {
                    console.log(clubs);
                }).catch(err => {
                console.log(err.message);
            })
        }).catch(err => {
        console.log(err.message);
    })
};
const makeRequest = async () => {
    try {
        let user = await getUserData();
        let clubs = await getClubsForUser(user.id);
        console.log(clubs);
    }
    catch(err) {
        console.log(err.message);
    }
};

When looking at the first example it gives you a headache. It’s easy to get lost in that nesting, braces. You can see in the second example that the code with async & await is more clean and easy to read. And also what we deal with is the callback hell. Async & await is a syntactical sugar for writing asynchronous code. It makes the code look and behave a little more like synchronous code but it remains to be asynchronous.

Conclusion

Some people may say that using async & await can make the asynchronous code less obvious. But C# had this feature for years and people who are familiar with it know it’s worth this minor, temporary inconvenience.

Top comments (21)

Collapse
 
t7yang profile image
t7yang

Wait, are you sure you know how to use promise?

This is totally same as your second example which writing in Promise

Obviously, nice and clean, Promise is better.

Collapse
 
savagepixie profile image
SavagePixie

Yeah, honestly the argument "async/await is better because I can write messy code using promises" is getting old. I can write really crappy code with any syntactical construct, but that doesn't make them any better or worse.

At the end of the day, I think it mostly comes down to style. If you prefer imperative code, you'll like async/await better because it's more imperative. If you prefer functional code, you'll like promises better.

Collapse
 
macsikora profile image
Pragmatic Maciej • Edited

First of all async await is a syntax sugar for Promises. Doing one vs another has not sense, both are in the same bandwagon. Secondly async/await can be more compared with Haskell do notation. Imperative code is about side effects, mutations. Until you don't do that you cannot say it's imperative code, therefore using async await vs raw promise is more code style

Thread Thread
 
savagepixie profile image
SavagePixie • Edited

First of all async await is a syntax sugar for Promises. Doing one vs another has not sense, both are in the same bandwagon.

That doesn't make them the exact same, though. Nor does it make them equivalent in all regards. Even if they are meant to achieve the same goal, they are expressed differently. This means that there's got to be some difference between them. So it makes sense to discuss their differences.

Secondly async/await can be more compared with Haskell do notation.

And Structs in Elixir can be compared to classes. That doesn't make them Object-Oriented.

Imperative code is about side effects, mutations. Until you don't do that you cannot say it's imperative code

Aren't most asynchronous operations side effects? Last time I checked I/O operations were considered side effects, for instance.
There's more to it than simply saying "imperative code is about side effects and mutations". As far as I'm aware, all programming languages deal with side effects in one way or another, however functional they are.
Besides, imperative code also uses, for instance, statements. In order to deal with errors with async/await, you use a try/catch statement. If you want the value of a variable to depend on whether an asynchronous operation is successful, you need to create a variable outside of the try/catch statement and then mutate it depending on the result. I'm no expert, but this doesn't sound very functional to me.

therefore using async await vs raw promise is more code style

I'm pretty sure that was my comment's point as well ;-)

Thread Thread
 
macsikora profile image
Pragmatic Maciej • Edited

Aren't most asynchronous operations side effects? Last time I checked I/O operations were considered side effects, for instance.

Yes, but you have referred that async/await is imperative, and Promises are not. They both are imperative, as they are doing side-effects, so there is no difference.

There's more to it than simply saying "imperative code is about side effects and mutations".

This is the only definition of imperative programming which doesn't break at the long term. Imperative programming needs to change state. You can use statements and write purely functional code. Statement are for sure constructs made for potentially side-effectfull programs, but it doesn't mean you can't use them in FP.

This is example of do notation:


do { putStr "Hello"
   ; putStr " "
   ; putStr "world!"
   ; putStr "\n" }

Its look and feel sequential. Does it mean it is imperative code? Its truly not. Does it mean it is not functional code, nope it doesn't either. "Do notation" is syntax sugar for chaining monads, the same as async/await is syntax sugar for chaining Promises.

Async/await was firstly introduced in functional language - F#. There is nothing non-functional in the concept itself.

That is true that in JS when we use async-await the code uses likely statements and try/catch, and truly these aren't concepts from functional world, but there is no difference between .catch in Promise chain and try/catch in async/await. Its the same thing but written differently.

Although I understand the sentiment that Promise chaining feels more like declarative programming, in reality both ways change the state and not declare the change like IO Monad in Haskell does, therefore both are imperative.

Collapse
 
leob profile image
leob

Spot on, I was about to write exactly this as a comment ... if you write the code not in a "nested" way but in a "chained" way (and with only one catch block, not two) then the resulting code looks surprisingly similar to ... the async await style code!

The way the code was written in the article mimics more the old "callback hell" style with its deep nesting. The point of promises is exactly that you can get rid of that.

You can argue that async/await is still marginally easier to read than promises but the difference is minor.

Where async/await really gets simpler is if you have loops and conditions. But there you need to watch out - a loop with async calls is not the same as Promises.all(), there are situations where you still may want to use the latter!

In other words, aync/await is great but you should still understand Promises as well (which is the foundation that async/await is built on).

Collapse
 
miksimal profile image
Mikkel

I agree that in this case, there's no need for async/await, but whenenver you have logic that needs to happen between the async requests, it makes things so much cleaner.

E.g. I'm writing a Lambda that scrapes a website, then creates an Item to put into a DB, then makes the async db put:

module.exports.scraper = async event => {
  const response = await got(URL);
  // pick the HTML elements I want, e.g. top 10 headlines
  // create the Item I want to put into db (10 headlines plus a date, e.g.)
  return await client.put(params).promise();
}

Also agree with @leob though that understanding Promises is still super. You may well need to combine the two as well, e.g.

const getStuff = async () => {
  const thingOnePromise = axios.get('thingOneAPI');
  const thingTwoPromise = axios.get('thingTwoAPI');

  const [thingOne, thingTwo] = await Promise.all([thingOnePromise, thingTwoPromise])
  console.log(thingOne + " and " + thingTwo);
}

(similar example given by Wes Bos in his talk on async/await which I highly recommend: youtube.com/watch?v=DwQJ_NPQWWo)

Collapse
 
t7yang profile image
t7yang

I'm not a async await hater, for me they are isomorphic which can transform to each other. I even like to mix using promise and async await.

The main problem of this article is the examples to judge async await is better than promise is pretty bad one.

Thread Thread
 
mjsarfatti profile image
Manuele J Sarfatti

Also, why is no one ever bothered by having to add that try/catch block... I don't understand. I hate it.

Thread Thread
 
t7yang profile image
t7yang

Yes, catch is much more elegant.

Thread Thread
 
sinestrowhite profile image
SinestroWhite

Can you explain why there must be a try/catch block?

@Manuele J Sarfatti

Thread Thread
 
mjsarfatti profile image
Manuele J Sarfatti • Edited

In a classic promise you have:

const request = somePromise()
  .then(data => doSomethingWithData(data))
  .catch(error => doSomethingWithError(error))

If you switch to using await and you do:

const data = await somePromise()
doSomethingWithData(data)

but the promise fails (throws an exception), then you have yourself an unhandled exception, and the browser will most probably crash your app.

The equivalent of the classic promise is therefore:

try {
  const data = await somePromise()
  doSomethingWithData(data)
} catch (error) {
  doSomethingWithError(error)
}

PS: this is pseudo-code off the top of my head and most probably not working, it's just to give an idea

Collapse
 
xowap profile image
Rémy 🤖

That syntax is a band-aid for the simplest problems. When you start trying to get conditionals or loops then you're fucked, and that happens very often in my universe.

Collapse
 
marceliwac profile image
Marceli-Wac

It's worth mentioning the execution within loops and higher order functions (map() etc.) how that is being handled by async/await and promises. There is a nice way of handling multiple async calls with promises, especially if they all run at the same time:

Promise.all(
  someArray.map(
    element => element.asyncCall()
  )
).then(() => console.log('Done!'))

the above is vastly different to the async/await style loops:

someArray.map(
  async (element) => await element.asyncCall()
)
console.log('Done!')

The latter will execute console.log('Done!'); without waiting for the element.asyncCall() calls.

Collapse
 
mvasigh profile image
Mehdi Vasigh • Edited

The two pieces of code are not equivalent though. In one you are wrapping the array in Promise.all, and in the second you are not. You can do the same thing by awaiting Promise.all using async/await syntax.

All async is, is just syntactic sugar over promises. If your code is not managing promises correctly it doesn't much matter which syntax you are using.

Collapse
 
marceliwac profile image
Marceli-Wac

My point was to show that the behaviour is different. I should have probably said "There is a nice way of handling multiple async calls with Promise.all, especially if they all run at the same time". Hats off for clarifying it though!

Thread Thread
 
mvasigh profile image
Mehdi Vasigh • Edited

I'm sorry, I am not trying to be nitpicky, but the behavior is actually not different at all. In both cases you are mapping an array to an array of Promises, but in one case you've wrapped it in Promise.all. I'm not trying to be hostile but just want to clarify it for others reading because this is a common mistake that JavaScript developers make with Promises.

In both cases you are mapping to an array of Promises, because element.asyncCall returns a Promise. That you are wrapping that in another Promise by making your lambda an async function and awaiting it does nothing other than to just wrap another Promise around it. The behavior is identical, the code is different.

EDIT: for example the non-async/await equivalent to your second example is actually roughly the following:

someArray.map(element => {
  return new Promise((resolve, reject) => element.asyncCall()
    .then(resolve)
    .catch(reject)
  )
})
Thread Thread
 
marceliwac profile image
Marceli-Wac

It's hard to disagree with that. Let me clarify this once again because it seems like I didn't explain what I meant quite clearly in either of those comments.

In my previous experience, especially in the beginning I have struggled to grasp the concept of promises, and using the wrapper syntax of async/await was something that I (incorrectly) settled for, instead of taking my time to study promises. That lead me to believe, that writing the second code (async function with awaited call passed into map) will work in the way identical to that described in the first example. It was the mistake that I have made and couldn't get to the bottom of prior to learning the Promises API and the Promise.all() function.

What I meant to say in my original comment was merely the fact, that awaiting asynchronous calls in async lambda (as passed to the map) does not work the same way as wrapping the array of promises in Promise.all() - hence "different behaviour". Yes, it is the same except for the Promise.all() part, which is what I was trying to illustrate from the start.

I'm sure that the example you just gave will further clarify this for any other readers. Thanks again!

 
miksimal profile image
Mikkel

Good point re. simply returning a promise being the same here, thanks! I just pasted from a Lambda I was writing where I had a try/catch block and was returning some messages, so I needed the await before and didn't even think about removing it. E.g.:

     await client.put(params).promise();
     return { message: "Success! The headlines were: " + topHeadlines }

Re. splitting the function that's a good idea, thanks. This was a copy/paste from a quick and dirty Lambda function for a personal project, but I like how your promise chain rewrite is both easier to read and more testable. Will think twice next time instead of lazily reaching for my async/await 💪

Collapse
 
grbolivar profile image
grbolivar

Good article. People just jump at your throat immediately, but all you needed to get your point across was a few lines of code:

let data = await fetch();

Vs

let data;
fetch().then(results => data = results);

Whoever prefers the second way just likes to write more code, not to mention async/await code is less clunky and easier on the eyes, heck, async/await may even result on smaller minified files.

Collapse
 
amitavroy7 profile image
Amitav Roy

Nice article and a nice analogy. Yes, I agree with you.. I have worked with Promises and async await is a lot more cleaner way of writing and maintaining code.

Coming from the world of PHP, it feels a lot more natural to me as well :)