Hacker News new | past | comments | ask | show | jobs | submit login
How to chain multiple functions in JavaScript properly with async/await (nikodunk.com)
60 points by nikodunk 61 days ago | hide | past | web | favorite | 23 comments



The comments about a function call waiting until an argument is defined is a potentnially misleading way of thinking about await. The calls to saveToCloudFirestore and sendEmailInSendgrid do not execute in parallel. await only really replaces .then()s and can't (as far as I know) achieve an effect like Promise.all()

I don't understand the line:

> return; // the return is only here because .then() callbacks need a return

I have never seen anyone do this before and have seen many .then() callbacks work. As far as I can tell `return;` is equivalent to letting the function reach the end.

Nitpick: in the sendEmailInSendgrid function, msg is never defined. I assume res.msg or res was meant.


> await only really replaces .then()s and can't (as far as I know) achieve an effect like Promise.all()

Although it's not completely equivalent to a Promise.all, you can actually use vanilla await with basically the same effect as promise.all(). Like so: [0]

  const timeout = ms => new Promise(res => setTimeout(res, ms));
  
  (async () => {
    const startTime = Date.now();
    const first = timeout(1000);
    const second = timeout(500);
    const third = timeout(800);
    await first;
    await second;
    await third;
    console.log(`Function completed in ${Date.now() - startTime} ms`)
  })();
This should output the completion time as somewhere very near to 1000ms. The reason they aren't completely equivalent is if one promise fails in Promise.all they all immediately fail whereas this only throws once you await the failed promise. Of course you could also use `await Promise.all([...])` in async/await too. You could try this in your browser console (I got it to log the output in chrome, but not safari for some reason) or you could go to link [0] and try it on jsfiddle.

[0]: https://jsfiddle.net/johnsonjo4531/r5fasqdo/1/


Note that this won't work concurrently if the promises are some kind of "lazy" promise implementation that executes the operation only when `.then` is called. Like with Knex, for example. I prefer

    const [a, b] = await Promise.all([promiseA, promiseB])


> Every async function needs a new Promise, and needs to resolve(). It won’t complain if you don’t do this, but it also won’t actually wait. The debugging around this is super annoying.

Async functions manage the "outer" promise automatically for you. It looks like you are "double-wrapping" promises unnecessarily by creating manual promises around a library that already looks to be returning Promises (or at least, is thenable and Promise-like).

So far as I can tell, it could be simplified to:

    async function getEmailOfCourseWithCourseId(courseId) { // async important
        try {
          const course = await doAsyncStuffWithFirestore(courseId)
          return course.email
        } catch (error) {
          console.error(error)
        }
    }

    async function sendEmailInSendgrid(fields, courseEmail) { // async important
      try {
        const msg = {to: courseEmail, from: fields.from, text: fields.text}
        await doAsyncStuffWithSendGrid(fields, courseEmail)
        return msg
      } catch (error) {
        console.error(error)
      }
    }

    async function saveToCloudFirestore(fields, courseEmail, courseId) { // async important
      try {
          return await doAsyncStuffWithFirestore(fields, courseEmail, courseId)
      } catch (error) {
        console.error(error))
      }
    }
The thing that stands out refactoring it to use awaits is that your try {} catch {} may be too low and you should move the try {} catch {} up higher in your call stack. (Do you really want to ignore the error and continue with all of these inner async functions? Because that is what you are currently doing.)


This is an excellent comment. Thank you! I see what you mean – I am double-wrapping with Promises and could replace this with try{} catch{}. My error was that I previously simply wrote

   return course.email
or whatever, which as far as I could tell did not make the outer function wait.

I will correct this in the article once I've tested it.


Yeah, the trick to remember is the await in the line before (and in general, anywhere you would .then() you can await, .catch() you can try/catch). You'll notice you can even `return await`, and that may have been the particular missing part you wanted. Where promises typically auto "flatten" their returns (return a Promise<T> in .then() and the return of .then() is a Promise<T> not a Promise<Promise<T>>), async/await does not flatten the promise automatically in case you meant to do that.

If you are writing a lot of async/await code, you may want to take the time to configure your linter for promises/async/await or try Typescript, as either or both can be very useful tools at spotting cases where you missed an await before a Promise.


Also a thing to note if you want just like with normal try catches you could catch certain errors that you know how to handle in that higher level of the call stack and you can rethrow errors that you don't know how to handle using `throw <error-variable-name-here>` to be handled in a lower part of your call stack.


> For readability’s sake, I have removed try/catch wrappings here that you should be doing in practice. You should never not catch errors, but makes the async/await concept way easier to understand.

On the contrary, you should never catch errors unless you’re actually going to handle them. A try/catch silently ignoring failures means you don’t care about the operation’s result. The default should be bubbling up errors to a centralized error handler.

Async functions make this even more convenient as all errors now become promise rejections.


One of the biggest gains in moving to async/await is the fact you can have a single try/catch for both synchronous and asynchronous code. This makes async functions which implement some kind of transaction much easier to implement.


> savePromiseDone && emailPromiseDone ? res.send() : null // sync, will wait until emailPromiseDone and savePromiseDone are defined ie. their functions are done

I'm not sure this is correct. Getting to this line already implies they are done, as the await will block. You could just put res.send() on this line no?


Yep, the comment is wrong, this line won't wait for anything but rather check that both calls resolved to a truthy value.

There are also some other incorrect things in that post that makes the code overly complicated, like:

> Every async function needs a new Promise, and needs to resolve()

That's definitely not needed unless you call something that runs asynchronously without being `async` or returning a standard `Promise` (in which case you could probably use a generic wrapper to convert them to `Promise` objects and avoid having to do that everytime).


My understanding was that each await would "block" (creates a new function in a then block that will get execed once the promise resolves). if you wanted to run the two in parallel you would have to Promise.all([promise1, promise2]).then(fn)


Hey gang!

I've always found it easier to think through async operations with .then(), but recently decided to make the switch. Totally worth it for conciseness! I thought I'd share my learnings above.

Suggestions welcome!


Hey, something is wrong with your interpreter or your transpiler if you need to wrap every `async` function in a `return new Promise`. If you mark a function as `async` it will do it automatically - in fact, if you look at e.g. `nodent`'s output, it's exactly how it does it. Calling the `resolve` function instead a `then` callback like that is a mess, and totally not needed. Also, the `return` is not necessary, every function has an implicit `undefined` return if not explicitly stated.

Moreover, in async functions you can take advantage of regular `try`/`catch` when awaiting; it seems you're just swallowing errors in your functions, which I guess is fine for the code you're showing. But keep in mind that a better practice is to let those function throw and catch it with a try/catch at the callsite.

Lastly, when calling the functions to save and send the email, you can use `Promise.all` and parallelize them instead of running them serially, as they don't seem to depend on each other. If you're meaning to send the email only after it is saved, then you need to check the return value of the saving before that; because neither of the calls will ever fail; just return undefined.


Ah, excellent points. Thank you for the very carefully crafted comment. I will update once I've re-tested with these changes.


Forgive me for this simple and unrelated question, but I'm new to JS development. This line right here, what is this?

let courseId = fields.to.substring(0, fields.to.indexOf('@'));

I've never seen this 'to' string method and I can't find a doc page for it. Can you explain it or point me towards a reference? Thanks


I haven't read the article, but there's no special significant to the `to` here. It is just looking up the value of a key named `to` in an object called `fields`, using [dot notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...):

  const fields = {
    to: "foo@bar.com"
  };

  fields.to  // "foo@bar.com


Just a note that if you're using AWS Lambda, arc.codes has native async function support, without callbacks. Ie, you just return a response instead of running res()

https://arc.codes/guides/http

And some cool middleware that's also await based:

https://arc.codes/guides/middleware


I still find async.js to be a superior tool for complex use cases than promises, async/await or callbacks. Think about it. A higher level abstraction designed for specific use cases, as for example to do sequential or parallel async operations is a far better approach imho than cobbling the same together using generic async language feature of choice.


Hi the sendgrid resolves variable msg but accepts the var res, is this intentional? :)


Good point! Thank you! Added.


A blog post is not a Show HN, so we've taken that out of the title. This is in the rules: https://news.ycombinator.com/showhn.html.


thank you!




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: