JavaScript acync/await part 2: Using async/await

JavaScript acync/await part 2: Using async/await

20 Nov 2019 javascript promises

Welcome back to this two-part series on async/await and, more broadly, the quest for synchronous-looking code that hides away asynchronous capability.

In part 1 we looked at how we got here, previous attempts to solve this problem, and how they worked out. In this part we'll be focusing purely on how to use the fantastic async/await combo.

The async keyword 🔗

The async keyword is used to tell the JavaScript engine that a function plans to use the await keyword in its body.

async function foo() { return 'bar'; }

It also ensures that the function returns a promise. Any return value the function spits out will be used as the resolved value of that promise. Let's take a look:

foo().constructor.name; //Promise foo().then(r => alert(r)); //"bar"

We could rewrite the above function with a non-async function by manually creating and returning the promise inside of it, and immediately resolving it with the value "bar".

function foo() { //normal func return Promise.resolve('bar'); } foo().then(r => alert(r)); //"bar"

But why do we need a function at all? Can't we just use await in top-level code? No! Any time you plan to use await you must be inside a closure declared with async - even if it's a function expression such as an immediately-invoked function expression (IIFE):

(async () => { //we can use 'await' in here })();

Operating inside a closure is good practice anyway; writing code in the top-level, outside of a closure is generally a bad idea as it leads to leakage and possible conflict with other, competing scripts.

The await keyword 🔗

Remember, as we saw above, to use await you must be in a closure created with async. I'll omit that from the following examples for brevity.

The await keyword is used to mark the point where execution within the current closure (only) should pause. await expects the value to the right of it to represent a promise (or a then'able object).

Execution resumes once the promise is resolved, and any assignment (to the left of the keyword) receives the resolved value.

let animal = await new Promise(res => setTimeout(() => res('dog'), 500)); console.log(animal); //"dog"

There, we wrap a setTimeout() call in a promise, and when the timeout's callback fires half a second later, it resolves the promise with the value 'dog'. Because we prefixed this with await, our assignment operation to animal assumes this value.

If the value to the right of await is not a promise, it'll implicitly wrap it in one and resolve it immediately with that value, the same way, as we saw above, that an async function wraps any return value in an immediately-resolved promise:

let i = await 5; console.log(i); //5 - value of immediately-resolved, hidden promise

Asynchronous functionality, synchronous syntax 🔗

Now we know how to use async and await, we can put them together to do what they were intended to do - to hide away asynchronicity as an implementation detail behind ostensibly synchronous(-looking) syntax.

Let's rework our request() function from part 1 to work with the fetch API which, as mentioned previously, returns a promise. We'll padd it out so that:

  • It receives data as an object which will be sent as the request body
  • It sends the request via the POST method
  • It expects JSON as the response format
function request(uri, params) { return fetch(uri, { method: 'post', body: JSON.stringify(params) }) .then(r => r.json()); }

Now let's suppose we need to do two requests to power a login flow:

  • First we'll grab the entered username and password and resolve that to a user ID
  • Second we'll pass that user ID to another endpoint to retrieve data about the user

With the power of async/await and fetches/promises, we can do this without breaking our synchronous-looking flow.

(For brevity, we'll assume we've already grabbed the entered user and pass in variables of the same names.)

(async() => { let data1 = await request('/app/login', {user: user, pass: pass}); let data1 = await request('/app/user_data', {id: data1.user_id}); console.log(data2); })();

To make sense of this, let's take a look at the shape of our server responses to our two requests (expected to be JSON, remember).

Request 1 (login based on passed user and pass):

{"user_id": 12345}

Request 2 (get user data based on passed user ID)

{ "first_name": "Dmitry", "last_name": "Shostakovich", "nationality": "Russian", ... }

And all is well! Unless, of course, we hit any errors...

Error handling 🔗

So far, to stay focused and in the interests of brevity, we've deliberately left out error handling. But it rarely pays to assume things will go well - particularly in code - so let's look at that now.

The key thing to know is that if the await promise is rejected - manually, or for example if a fetch() request throws an error - code execution will not resume (within the closure that the await keyword is within - not globally) unless it is handled.

function doSomethingAsync() { return new Promise((resolve, reject) => { setTimeout(() => reject('Oops!'), 500); }); } let i = await doSomethingAsync(); console.log('Continue'); //uncaught exception - we never get here!

So our alert() line never happens, because the await promise is rejected but the error uncaught. What happens instead is the error will be sent to the console as an uncaught exception. That's definitely not ideal, so let's do some handling.

We can do this in two ways. One is to wrap our attempt in a good old try-catch block.

try { let i = await doSomethingAsync(); } catch(e) { console.error(e); } console.log('Continue'); //this happens - we caught the exception!

Much better. Now, even if our await promise is rejected, the exception thrown is caught by our catch block and normal execution can resume.

But we're using promises, remember, and promises have a rich API including a structured way to handle errors. We can do this by chaining a catch() call to it.

function doSomethingAsync() { return new Promise((resolve, reject) => { setTimeout(() => reject('Oops!'), 500); }) .catch(e => console.error(e)); } let i = await doSomethingAsync(); console.log('Continue'); //happens - we caught the error

We can write this another way, too. Chaining catch()-wrapped callback to a promise is the same as passing on-resolution and on-rejection callbacks to a chained then() method.

return new Promise((resolve, reject) => { setTimeout(() => reject('Oops!'), 500); }) .then( resValue => alert(value), rejReason => console.error(rejReason) );

Top-level await 🔗

As of ECMAScript 13 (2022) it's now possible to use await in the top-level, rather than, as before, only in asynchronous callbacks - provided your code is running as a module.

//line 1 of a JS file running as a module await fetch('https://example.com/etc');

Concurrent requests 🔗

So we've seen how amazeballs async/await is for writing synchronous(-looking) code which hides away asynchronous operation as an implementation detail. We fire off a request, wait for its resolution or rejection, then fire off another, and so on, all the while looking completely synchronous.

This is fine for times where the order of request completion matters. For any other time, it's not; making each request wait for the previous one to complete is inefficient.

Suppose we wanted to get the latest product offers from various categories. The request order isn't important; we just need to know when they're all done. Can we still use async/await? You bet! We can use it in conjunction with Promise.all().

Promise.all() takes an array of sub-promises as its only argument, and returns a master promise that resolves when all those sub-promises are settled! Let's take a look. Once again we'll use our request() function.

let categories = ['books', 'dvds', 'games'], promises = categories.map(cat => request('/app/get_offers/', {type: cat})), offers = await Promise.all(promises); console.log(offers); //got all offers!

So first we declare our categories, then we map that array of categories to an array of promises (created and returned by our request() function). Notice how we haven't used await yet - we don't want to stop code execution (i.e. we don't care about settlement order) - we just want to fire off the requests.

Now for the key part. Where we do want to pause code excecution is the part before we gather the resolution values of those requests, and so we use it right before Promise.all().

In other words, we don't want to run our console.log() line until Promise.all() reports that all sub-requests fed to it have settled, and gives us the resolved values.

Again let's model our server responses so we can visualise this better. Suppose it looked like this for each of our three requests (modifying the content accordingly):

[{ "id": 12345, "title": "French Revolutions", "author": "Tim Moore", "price": 9.99 }, { "id": 67890, "title": "When You are Engulfed in Flames", "author": "David Sedaris", "price": 8.99 }]

...then our console.log() would output an array containing three sub-arrays (one per request, each being the resolution value of the request), containing products in the structure shown above.

Summary 🔗

async/await is a great way to write synchronous-looking code that hides away asynchronous operation as an implementation detail.

async tells a closure that await will be used inside it, and ensures that it returns an implicit promise, using it to wrap any return value as the promises's resolution value.

await tells code execution, in the current closure, to pause at that point and first resolve the promise to the right of it, feeding its resolved value to the assignment to the left of it. If the value to the right is not a promise (or then'able object), it will be implicitly wrapped in one and immediately resolved.

Any uncaught exceptions (if the promise is rejected) will result in code execution not resuming; exceptions can be caught via try-catch blocks (around the await) or via chained promise methods such as catch().

I hope you found this guide useful!

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!