async and await in Node.js— 1000m

Emma O'Donnell
6 min readFeb 28, 2021

This is the first in a three part series on the async and await keywords in a Node.js application. In each part we’ll dive deeper into what these keywords do with the idea being that:

This is the 1000m view so we’ll aim to keep it concise and useful.

Why run async code?

Note: The value of async is well-trodden ground, if you’re not sure on the distinction between async and sync code there’s a ton of resources around. If my explanation doesn’t gel with you, Google til you find something that does

Let’s say you have a super simple application. It is a UI with two elements — the current time (hh:mm:ss) and the current temperature. The UI queries a Node.js server to get the current time every second and to get the current temperature every hour.

A comically basic UI displaying the current time and temperature in degrees celcius
I am a professional software engineer

The Node.js server is the thing we care about here. It has two methods:

  1. getTime() —Gets the current time using new Date().getTime()
  2. getTemperature() — Calls on a third-party API for the weather. Extracts temp from that weather response and then returns it.

Assume the third-party API is really slow —it takes 10 seconds or so to fulfil each request. Your server has two options when fulfilling the getTemperature() request:

A) Synchronous:

  1. Ask the third-party API for the weather
  2. Neglect other duties to diligently wait for third-party API response
  3. Get response and return temperature to UI
  4. Handle the ~10 getTime() requests that have backed up since server started waiting

B) Asynchronous:

  1. Ask third-party API for the weather
  2. Server works on whatever else it has to do. For example, when getTime() requests come in it fulfils those right away
  3. Periodically check to see if the third-party API has responded, if not, go back to doing other stuff
  4. Eventually server will see the third-party API has responded, at that point parse response and return temp to UI

In Option A we described blocking. This is where your application is stuck waiting for something without actually doing any work itself. It’s actually pretty hard to block like this in JavaScript, but if you pulled it off you’d notice that the clock in the UI would freeze for 10 seconds every hour. This is because the Node.js server would be busy waiting for the third-party API. During that time it would not be able to handle getTime() requests and the clock would stop ticking as a result.

Option B avoids blocking which is super important for real applications; you don’t want one user’s request to prevent others from being fulfilled or to prevent other background operations from running.

The main takeaway is that when your app needs to wait for some result to be available this is something we want to do asynchronously wherever possible.

awaiting an async function

Okay so how do we write some async code?

The third party API I’m relying on has put out a package to help me call it correctly. The method I care about is client.getWeatherAsync(). The package developers have been really helpful and included Async on the end of the method name which tells me this is an async function. If I wanted to be extra careful though I could check to see that getWeatherAsync() returns a Promise. Any function which returns a Promise is awaitable!

I can await that async function like this:

When I await my third party API call on line 2, getTemperatureNowAsync() stops executing. It won’t resume execution until the API responds. This means that on line 3 (and beyond) we can use the results of that API call. It does all of this without blocking.

This looks really similar to synchronous code; in fact the only real difference is the inclusion of the word await. This is part of the reason async and await are so useful — you don’t have to switch styles when working with sync vs async code.

Two other things to note here. First getTemperatureNowAsync()is proceeded by the async keyword. You can only use await inside an async function. Second, getTemperatureNowAsync() returns a Promise. All async functions have to return a Promise. If you forget either of these points it’s no big deal: the IDE should remind you when you try to use await.

awaiting Promises

A Promise represents an asynchronous operation. Promises have 3 potential states:

  1. Pending (not finished yet)
  2. Resolved (async operation completed successfully)
  3. Rejected (something went wrong)

In the same way you can await an async function, you can await a Promise directly. Most of the time you don’t actually need to create your own Promise — if you write code inside an async function your return value will be wrapped in a Promise automatically — but occasionally the need may arise.

Most of the time though you’ll be awaiting a Promise returned by a function. Whenever you await an async function you’re actually awaiting the Promise it returns:

await myFunctionAsync();/************ same thing as *************/var myPromise = myFunctionAsync();
await myPromise;

If you await a Promise, execution of your function will pause while the Promise state is Pending. When the Promise state moves to Resolved, execution of the calling async function can continue.

I’m going to expand this example very soon to demonstrate that it’s non blocking. But what happens here is:

  1. We log “first” to the console
  2. 3 seconds pass
  3. We log “second” to the console
  4. We log “third” to the console

await async functions from a synchronous function

When you’re in a function which isn’t async, you can’t use the await keyword. Fairly often though you’ll need to kick off an asynchronous process from a synchronous function.

To do this just recall that your async functions return Promises. I can therefore use standard Promise syntax to trigger my async code from a non-async setting. Something like the following works to delay execution of code until after a Promise resolves:

callStuffAsync()
.then(() => {
console.log("do this afterwards")
})

If you’re uncertain about this syntax I’d encourage you to Google around it — Promise syntax is beyond the scope of this article. We use await and async because this kind of syntax is a little less concise and (at least for me) a little harder to read. When the need arises though you can fallback on something like this to trigger your async code from a non-async function.

Knowing this allows us to expand the example from earlier to see that awaiting an async function (or a Promise) is non-blocking:

Can you follow the console logs here to get insight into how await is working?

  1. On line 16 invoke callStuffAsync()
  2. Move to line 11 and then log “first
  3. Start awaiting wait3SecondsThenLog() on line 12, callStuffAsync() yields the Thread and execution moves on to line 19. Log “second
  4. After three seconds, the timeout is up and “third” is logged.
  5. wait3SecondsThenLog() has resolved, so callStuffAsync() can resume execution and log “fourth
  6. callStuffAsync() has resolved. We can therefore move into the callback on line 17 and log “fifth

If we removed await from line 12 what would the log output be? (See “Knowledge test answer” at the bottom of the article)

async errors

I mentioned earlier that Promises have 3 states: Pending, Resolved, Rejected.

When a Promise is Rejected it means something’s gone wrong — how do we handle this?

How to handle a rejection from within an async function?

Within an async function you just need to wrap your awaited call inside a try catch block. A failed, awaited Promise will throw an error in much the same way synchronous code would:

In a similar vain, if you throw an Error from within an async function the Promise returned by that function will move to the rejected state.

Main Takeaways

  1. async methods allow you to use await statements.
  2. All async functions must return a Promise
  3. Only a Promise can be awaited
  4. Awaiting a Promise stops the async client-function from continuing execution until the Promise has resolved
  5. Wrap Promises in try-catch blocks to handle failures
  6. It’s generally good to await async things when possible

Knowledge test answer: first, fourth, second, fifth, third — If you expected “fifth” to log before “second” you’re not alone. To find out why this isn’t the case you can check out the second article in this series… but honestly it’s mostly just important that you understand things will happen out of order without await.

Other articles in this series

Async and await in Node.js — 100m

--

--