Alxblsk.com Logo
RE•DONE
Blog by Aliaksei Belski

Asynchronous operations in Javascript: async/await

Published: June 14th, 2020, Updated: September 12th, 2020javascriptasync/awaitECMAScript2016

Asynchronous operations in Javascript: async/await

In the first part, we've reviewed a mechanism of Promises, introduced as a part of ECMAScript 2015 (and polyfilled years before).

A year later, with ECMAScript 2016 and asynchronous functions, we’ve got an opportunity to write asynchronous code in a more familiar, synchronous-like way, thanks to await keyword. This changed an approach of handling asynchronous actions once again, and still, not everyone got used to it.

What an asynchronous function is?

Do you remember I mentioned a so-called capsule while talking about Promises? It holds a value once Promise is created and allows us to process a result only inside of it. So, async function is the representation of that capsule. Every function declared as async returns a Promise, no matter whether you return it or not. If you don't, Javascript will wrap a return value with a Promise by itself.

async function test() {
  return 'OK'; // Promise {<resolved>: "OK"}
}

async function fail() {
  throw new Error('random'); // Promise {<rejected>: Error: random
}

Once defined, it can be called differently, by using await keyword.

async function testWrapper() {
	const response = await test();
	console.log(response);
}

But why it's needed?

Straight away async/await looks as syntactic sugar for a Promise. And it definitely is. This syntax brings us an opportunity to work with asynchronous code as it'd be a regular synchronous code. In addition to this, new syntax opens the following opportunities:

  • Handle errors with try/catch syntax;
  • Get an exact value stored in a Promise (extract a value from a capsule).

Visualization of async-await mechanism in Javascipt

Handling exceptions

Due to syntax change, it is important to understand on how to handle exceptions correctly. Let's review and example:

const request = async () => {
  const response = await fetch('https://alxblsk.com/rss.xml');
  const data = await response.text();
  
  const xmlData = (new window.DOMParser()).parseFromString(data, "text/xml");
  return from(xmlData.getElementsByTagName('item'));
}

const render = async () => {
  let tree;
  
  try {
    tree = await request();
  } catch (ex) {
    console.warn(ex);
    tree = [];
  }

  return tree;
}

// call render() here

Notice we perform two asynchronous actions here: first requests an XML from a server, while the second one parses a body of a response.

  • Both of them are async and require await before a call;
  • Both of them can fail by some reason (404, empty response).

But here is the thing: it's not necessary to handle those errors inside of request function.

  1. If an error happens, it's wrapped with Promise.reject() and can be handled on a top-level;
  2. If an error is quietly suppressed (e.g. parse error), nothing will be found in resulted operation;
  3. If everything is fine, the correct result will be returned.

You may notice that with those two separate functions code may look a bit verbose. Indeed. However, other factors become important here: readability, supportability, and testability. In the era of high speeds and smart obfuscation mechanisms, the latter may be much more important than 5 saved lines of code.

More Facts

  • Class methods can be async as well.
  • Any async function can be called only inside of another async function.
  • Thus await can be used only inside of async function as well.
  • So use try/catch on a top-level.

Other posts in a series