In today’s video I’ll be demonstrating the usage of async/await in JavaScript (asynchronous functions) and how they give you a much cleaner way to work with Promises.


JavaScript Async/Await and Promises

Anyone who calls themself a JavaScript developer has had to, at one point, work with either callback functions, Promises, or more recently, Async/Await syntax. If you’ve been in the game long enough, then you’ve probably seen the transition from the days where nested callbacks were the only way to do asynchronous JavaScript.

When I first started learning and writing JavaScript, there were already a billion guides and tutorials out there explaining how to use them. Yet, many of them were just explaining how to convert between callbacks to Promises, or Promises to Async/Await. Admittedly, for many, that is probably more than enough for them to get along and write code.

However, if you are like I was, and really wanted to understand asynchronous programming (and not just JavaScript syntax!), then perhaps you’ll be in agreement with me that there is a shortage of content out there actually explaining asynchronous programming from the ground up.

What does “asynchronous” mean?

Generally, the answers you’d get by asking that question are something of the following lines:

  • Multiple threads executing code at the same time
  • More than one bit of code being run at once
  • Concurrency(?)

And these would all be somewhat sort of along the right lines. But instead of giving you a technical definition you’re probably going to forget later anyways, I’m going to give you an example a five year old could understand.

A more human analogy

Imagine you’re cooking some vegetable soup. For the sake of a nice and simple analogy, let’s assume said vegetable soup only consists of onions and carrots. The recipe for such soup may be something of the following:

  1. Chop some carrots.
  2. Chop some onions.
  3. Add water into a pot, turn the stove on and wait for it to boil.
  4. Add carrots into a pot and let it cook for 5 minutes.
  5. Add onions into a pot and let it cook for another 10 minutes.

These instructions are simple and clear, but if any of you out there reading this actually do cook, you’ll realize this isn’t the most time-efficient way to cook. For those of you who aren’t experienced cooks, here’s why:

  • Steps 3, 4, and 5 don’t actually require you as the chef to do anything, other than watch the pot and keeping track of the time.
  • Steps 1 and 2 do require you to actively do something.

Hence, a more adept chef might do the following:

  1. Start boiling a pot of water
  2. Whilst waiting for the pot of water to boil, start chopping up some carrots.
  3. By the time you finish chopping carrots, the water should be boiling, so add the carrots.
  4. Whilst the carrots are cooking in the pot, chop up the onions.
  5. Add the onions and then cook for another 10 minutes.

Whilst there’s still the same number of things to do, you can hopefully see how this would be much faster and more efficient. This is exactly the same core concept as asynchronous programming: You never want to sit around purely waiting for something whilst there are other things you can spend your effort on.

And we all know that in programming, waiting happens pretty damn often — whether it be waiting for an HTTP Response from a server, filesystem I/O, etc. But your CPU’s execution cycles are precious and should always be spent actively doing something**,**and not waiting around: hence, Asynchronous Programming.

Now, let’s get to actual JavaScript, shall we?

Okay, so sticking to the same example of vegetable soup, I’m gonna define a few functions to represent the actions above.

First let’s define our normal, synchronous functions representing tasks that don’t require waiting for anything at all. These are your good ol’ plain JavaScript functions— though do note that I simulated chopCarrots and chopOnions being tasks that require being actively worked on (and taking time) by letting them do some expensive computations¹. I’ve commented out the actual implementations since they don’t really matter, but they’re available in the footnotes¹.

function chopCarrots() {
  /**
   * Some long computational task here...
   */
  console.log("Done chopping carrots!");
}

function chopOnions() {
  /**
   * Some long computational task here...
   */
  console.log("Done chopping onions!");
}

function addOnions() {
  console.log('Add Onions to pot!');
}

function addCarrots() {
  console.log('Add Carrots to pot!');
}

As for the asynchronous functions, first I’ll quickly explain how JavaScript’s type system handles asynchronicity: basically, all results (including void ones) from asynchronous operations must be wrapped in a Promise type.

In order to have a function return a Promise, you can either

  • Explicitly return a Promise i.e. return new Promise(……);
  • Add async to the function signature
  • or both of the above.

For reasons² I’m not going to get into this article, you should always use the async keyword on asynchronous functions.

So for our asynchronous functions (representing steps 3–5 of making vegetable soup):

async function letPotKeepBoiling(time) {
  return /* A promise to let the pot keep boilng for certain time */
}

async function boilPot() {
  return /* A promise to let the pot boil for time */
}

Again, I’ve removed details of the implementation, but I’ll post them at the end if you want to see them¹.

Also important to know is that in order to wait for the result of a Promise so you can do something with it, you can simply use the await keyword:

async function asyncFunction() {
  /* Returns a promise... */
}

result = await asyncFunction();

So now, we just need to put this all together:

function makeSoup() {
  const pot = boilPot();
  chopCarrots();
  chopOnions();
  await pot;
  addCarrots();
  await letPotKeepBoiling(5);
  addOnions();
  await letPotKeepBoiling(10);
  console.log("Your vegetable soup is ready!");
}

makeSoup();

But wait! It doesn’t work! You’ll get a SyntaxError: await is only valid in async functions. Why? Because, if you don’t declare a function as async, then by default JavaScript takes that to mean it is a _synchronous_function — and synchronous means no waiting!³ (This also means you can’t use await at the top level script outside of a function at all).

Hence we simply add the async keyword to the makeSoup function:

async function makeSoup() {
  const pot = boilPot();
  chopCarrots();
  chopOnions();
  await pot;
  addCarrots();
  await letPotKeepBoiling(5);
  addOnions();
  await letPotKeepBoiling(10);
  console.log("Your vegetable soup is ready!");
}

makeSoup();

And voila! Note that, on line 2, I call the asynchronous boilPot() function without an await keyword, because we don’t actually want to wait for the pot to boil before starting to chop up carrots. We only await the Promise(d) pot before we need to put in the carrots into the pot, since we don’t want do that before the water is boiling.

What happens during await calls? Well, nothing……kind of….

In the context of the makeSoup function, you can simply think of it as you are waiting for something to happen (or a result to eventually be returned).

But, remember : you (the CPU) never want to sit around purely waiting for something whilst there are other things you can spend your effort on.

Hence, instead of just making soup, if we had this instead:

makeSoup();
makePasta();

Then whilst we are await-ing letPotKeepBoiling, we could actually be cooking pasta as well.

See? Async/Await is actually quite easy to use once you understand it, isn’t it?

What about explicit Promises?

Alright, if you insist, I’ll go over explicitly using Promises as well. Keep in mind, the async/await methods are based on promises themselves and hence the two methods are fully compatible.

Explicit promises are, in my opinion, the half-way between using old-style callbacks and the new sexy async/await syntax. Alternatively, you can also think of the sexy async/await syntax as nothing more than implicit Promises. After all, async/await came after Promises, which itself came after callbacks.

So take a brief visit in our time machine to callback hell:

function callbackHell() {
  boilPot(() => {
    addCarrots();
    letPotKeepBoiling(
      () => {
        addOnions();
        letPotKeepBoiling(
          () => {
            console.log("Your vegetable soup is ready!");
          },
          1000,
        );
      },
      5000
    )
  },
  5000,
  chopCarrots(),
  chopOnions(),
  );
}

I’m not gonna lie, I wrote that on the fly as I was just writing this article, and it took me waaayyy longer than I’d like to admit it did. And many of you will have no idea what’s going on at all. My dear lord, aren’t all of those callbacks terrible? Let this be a lesson to never touch callbacks again…

And, as promised (hehe), using explicit Promises:

function makeSoup() {
  return Promise.all([
    new Promise((reject, resolve) => {
      chopCarrots();
      chopOnions();
      resolve();
    }),
    boilPot()
  ]).then(() => {
    addCarrots();
    return letPotKeepBoiling(5);
  }).then(() => {
    addOnions();
    return letPotKeepBoiling(10);
  }).then(() => {
    console.log("Vegetable soup done!");
  });
}

As you can see, some aspects of Promises are still very callback-y.

I won’t go into detail, but basically:

  • .then is a method on a Promise that takes the result of that Promise and passes it to the argument function (so essentially, a callback function…)
  • You can never unwrap the result of a Promise for use outside the context of .then.⁵ In essence, .then is like an async block thatawait-s for the result and then passes it to the callback.
  • There’s also reject callbacks for handling errors and the .catch method on Promises for handling errors, but I won’t get into that, as there are billion other Promises tutorials out there.

Conclusion

I do hope you’ve gained some understanding of Promises and asynchronous programming from this article, or perhaps at least learned of a nice way to explain it to someone else.

So, which one should you use? Explicit Promises or Async/Await?
The answer’s completely up to you — and I’d argue mixing them isn’t so bad either as both are fully compatible with the other.

That said, personally I’m 100% on the Async/Await camp, as to me the code is much clearer, and more reflective of the true multi-tasking nature of asynchronous programming.

Full source code available here.

#javascript #web-development

JavaScript Tutorial - Async/Await Explained with Examples
26.20 GEEK