Live data from Hacker News

A brief look at async-await

javascript.christmas

11–20 of 47 posts

Re: A brief look at async-await

#11
post #5

I appreciate that the author is trying to illustrate something with an intentionally contrived example, but there is really no need for "await" in his "chain()" function: function chain () { (function loop (i) { if (i > 5) { // set state to failed return; } check().then((result) => { if (!result) { return loop(i + 1); } // set state to finished }).catch((error) => { // set state to failed }); }(0)); } This is one of…

I agree. Promises solve superficial aesthetic problems that only beginners trip on and unnecessarily add dangerous hidden state and corner cases.

You can further simplify that chain() function by removing promises altogether:

    function chain () {
        (function loop (i) {
            if (i > 5) {
               // set state to failed
                return;
            }
            check((err,result) => {
                if(err){//set state to failed
                    return;}
                if (!result) {
                    return loop(i + 1);
                }
                // set state to finished
            });
        }(0));
    }
Callbacks are much simpler and less error prone:

https://medium.com/@b.essiambre/continuation-passing-style-p...

Re: A brief look at async-await

#12
post #5

I appreciate that the author is trying to illustrate something with an intentionally contrived example, but there is really no need for "await" in his "chain()" function: function chain () { (function loop (i) { if (i > 5) { // set state to failed return; } check().then((result) => { if (!result) { return loop(i + 1); } // set state to finished }).catch((error) => { // set state to failed }); }(0)); } This is one of…

> It depends on your field of work, of course, but in my experience these situations are really not that common. Very common in my field. A single function that loads a file, parses metadata, then transforms the binary data into a different format can consist of 2-4 async calls that need to be executed in order. Await is incredibly helpful here, and makes code much more readable compared to the chain of then statemen…

And your error handling? ;)

  fetch((err, res) => {
    if (err) {
      // yay - error handling! :)
    }

    const myJson = res.json();
    // do stuff
  }
What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!

Re: A brief look at async-await

#13

Earlier quoted context omitted.

> It depends on your field of work, of course, but in my experience these situations are really not that common. Very common in my field. A single function that loads a file, parses metadata, then transforms the binary data into a different format can consist of 2-4 async calls that need to be executed in order. Await is incredibly helpful here, and makes code much more readable compared to the chain of then statemen…

And your error handling? ;) fetch((err, res) => { if (err) { // yay - error handling! :) } const myJson = res.json(); // do stuff } What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!

As far as I can tell, json() also needs to be treated as a promise, and fetch doesn't take a callback as an argument Wouldn't the correct non-await version look like this?

    fetch(url).then( response => {
        response.json().then( json => {
            // do stuff
        });
    });
This is already much worse then the await version, without any error handling yet. And if I need to call another promise in order, I'd need yet another level of callback. Instead of two easily intelligible lines that could be wrapped into a single try/catch, we now have 4 lines and two new scopes/levels.

Why would I ever use this over await?

Re: A brief look at async-await

#14

Earlier quoted context omitted.

> It depends on your field of work, of course, but in my experience these situations are really not that common. Very common in my field. A single function that loads a file, parses metadata, then transforms the binary data into a different format can consist of 2-4 async calls that need to be executed in order. Await is incredibly helpful here, and makes code much more readable compared to the chain of then statemen…

And your error handling? ;) fetch((err, res) => { if (err) { // yay - error handling! :) } const myJson = res.json(); // do stuff } What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!

Parent's code with callbacks would be more like this:

  fetch((err, res) => {
    if (err) {
      // yay - error handling! :)
    }

    res.json((err, myJson) => {
      if (err) {
        // oh no - another error handler :(
      }
      
      // do stuff
    });
  }
fetch.json is async as well. With await the try / catch would just be around both await calls.

Re: A brief look at async-await

#15
post #14

Earlier quoted context omitted.

And your error handling? ;) fetch((err, res) => { if (err) { // yay - error handling! :) } const myJson = res.json(); // do stuff } What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!

Parent's code with callbacks would be more like this: fetch((err, res) => { if (err) { // yay - error handling! :) } res.json((err, myJson) => { if (err) { // oh no - another error handler :( } // do stuff }); } fetch.json is async as well. With await the try / catch would just be around both await calls.

Yes this is true, as long as your error handling is the same for all calls (not necessarily the case, especially if logging is involved).

I don't actually have a problem with the callback version because it is nice and explicit. But again, I did originally say say that "async/await" was actually a valid use case in this necessarily serial scenario (and is exactly what I do in my article that I linked to). ;)

That said, if I see a chain of serial calls more than a few levels deep it is a code smell that indicates refactoring is necessary. There really is no reason to write code like that unless it's very simple logic. This is why I have never had a problem with "callback hell" even though I write code that is heavily asynchronous (APIs, WebSocket, pub/sub etc.)

Re: A brief look at async-await

#16

Earlier quoted context omitted.

And your error handling? ;) fetch((err, res) => { if (err) { // yay - error handling! :) } const myJson = res.json(); // do stuff } What's so unreadable about this? Even if it is 4 calls it would still be readable because if you have any sizeable amount of logic you should be refactoring it into several functions anyway with the above function gluing them together. Much easier to write tests for too!

As far as I can tell, json() also needs to be treated as a promise, and fetch doesn't take a callback as an argument Wouldn't the correct non-await version look like this? fetch(url).then( response => { response.json().then( json => { // do stuff }); }); This is already much worse then the await version, without any error handling yet. And if I need to call another promise in order, I'd need yet another level of call…

you can chain them and avoid the nesting. Anything a .then handler returns becomes the value of the next one like this:

    fetch(url)
      .then((response) => response.json())
      .then((json) => {
        // do stuff
      })
Promises will wait until resolved, normal values will call the next handler "right away" (there's some nuance here and some edge cases about what "right away" means, but for the most part you never need to think about that)

And if you want to do more things and handle errors, it becomes pretty simple as well:

    fetch(url)
      .then((response) => response.json())
      .then((json) => {
        if (!json.user.id) {
          throw new Error('user id not found')
        } else {
          return db.sql('SELECT * from users where id = $1', [json.user.id])
        }
      }).then((user) => {
        return response.send(user)
      }).catch((err) => {
        // any error thrown at any point during the chain will trigger this catch
        return response.error(err)
      })
I still completely agree that async/await is still better in this case, but then throw some more wrenches into the situation like wanting to handle multiple promises at a time and you start to see where using "raw promises" really comes in handy. Like this:

    try {
      const res = await fetch(url)
      const json = await res.json()

      if (!json.user.id) {
        throw new Error('user id not found')
      }

      const [ userObj, userAuthLevel, someOtherStuff] = await Promise.all([
        db.sql('SELECT * from users where id = $1', [json.user.id]),
        db.sql('SELECT * from otherStuff where userId = $1', [json.user.id]),
        fetch('https://other.stuff/and/things')
      ])

      return response.send({
        userObj,
        userAuthLevel,
        someOtherStuff
      })
    } catch (err) {
      return response.error(err)
    }
or say there's an expensive call that you can start BEFORE the first fetch, but still need to wait on later (ignoring most other stuff for simplicity):

    // notice there's no await...
    // we can kick off the request now, but not wait for the result until later
    const someOtherStuffPromise = fetch('https://other.stuff/and/things') 

    const res = await fetch(url)
    const json = await res.json()

    // do other things here

    // finally wait for the promise to resolve here.
    const someOtherStuff = await someOtherStuffPromise

Re: A brief look at async-await

#17
post #9
post #6

Earlier quoted context omitted.

> This is one of my pet peeve with proponents of promises actually. They come up with convoluted examples to argue against callbacks but it's not the callbacks that are a problem, but the person writing the code. While I agree the author’s examples are convoluted, the async-await code is much easier to read, write, and validate. Why bother tracking state and having to keep tracking of JS lexical scoping when you can…

> the async-await code is much easier to read, write, and validate. With the exception of necessarily serial code that I referred to, this statement is not true in my experience. Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. In the real world each call may need different logging or even error handling logic and that is when promises reall…

>Your "legible" example is only legible because you are not doing anything actually important like error handling and logging.

    async function doStuff() {
      try {
        // Fetch things concurrently
        const [
          foos,
          bars,
          bazs,
        ] = await Promise.all([
          getFoos().then((res) => {
            console.log(`getFoos done with ${res}`
            return res
          })),
          getBars(),
          getBazs()
            .catch((err) => {
              // handle only the error in getBazs
            }),
        ]);
        // Do stuff with them...
      } catch (err) {
        // handle any errors
      }
    }

it still isn't what i'd call "pretty" code, but it's simpler for me to read and comprehend than doing most of those things without async/await or Promise.all or other helpers like that.

Re: A brief look at async-await

#18
post #9

Earlier quoted context omitted.

> the async-await code is much easier to read, write, and validate. With the exception of necessarily serial code that I referred to, this statement is not true in my experience. Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. In the real world each call may need different logging or even error handling logic and that is when promises reall…

>Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. async function doStuff() { try { // Fetch things concurrently const [ foos, bars, bazs, ] = await Promise.all([ getFoos().then((res) => { console.log(`getFoos done with ${res}` return res })), getBars(), getBazs() .catch((err) => { // handle only the error in getBazs }), ]); // Do stuff with…

I was about to write up that exact example of the nested handlers.

Re: A brief look at async-await

#19
post #9

Earlier quoted context omitted.

> the async-await code is much easier to read, write, and validate. With the exception of necessarily serial code that I referred to, this statement is not true in my experience. Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. In the real world each call may need different logging or even error handling logic and that is when promises reall…

>Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. async function doStuff() { try { // Fetch things concurrently const [ foos, bars, bazs, ] = await Promise.all([ getFoos().then((res) => { console.log(`getFoos done with ${res}` return res })), getBars(), getBazs() .catch((err) => { // handle only the error in getBazs }), ]); // Do stuff with…

(see below)

Re: A brief look at async-await

#20

Earlier quoted context omitted.

>Your "legible" example is only legible because you are not doing anything actually important like error handling and logging. async function doStuff() { try { // Fetch things concurrently const [ foos, bars, bazs, ] = await Promise.all([ getFoos().then((res) => { console.log(`getFoos done with ${res}` return res })), getBars(), getBazs() .catch((err) => { // handle only the error in getBazs }), ]); // Do stuff with…

(see below)

But those 2 examples aren't doing the same thing.

My Promise.all example will fire off the getFoos, getBars, and getBazs requests all at the same time, and then do things at different moments with the results in some cases. Yours will do them serially like this:

getFoos -> wait for it to resolve -> run getBars -> wait for it to resolve -> run getBazs -> wait for it to resolve -> run doStuff

And if individually handled errors is what you are after ala go, then this also does it while preserving the parallelism:

    async function doStuff() {
      // Fetch things concurrently
      const [
        foos,
        bars,
        bazs,
      ] = await Promise.all([
        getFoos().catch((err) => {
            // handle only the error in getFoos
          }),
        getBars().catch((err) => {
            // handle only the error in getBars
          }),
        getBazs().catch((err) => {
            // handle only the error in getBazs
          }),
      ]);
      // Do stuff with them...
    }


And I'm not suggesting we always use promises in every area, but that for some things they are the perfect tool for the job. There are some things that promises are really bad at that callbacks do really well. One example is progress systems.

A callback can be called multiple times over the course of an async call with percentage it's completed every call. A promise is one-and-done, and that's a big issue in many areas. But that's why callbacks aren't really "deprecated" as much as they are relegated to doing what they do best, while leaving things that Promises and async/await do best to them.

Post reply on HN