Async Await

Async functions are higher level abstractions around generators and promises that can be used to simplify asynchronous flows. Any JavaScript function definition can be marked with the async keyword. When a function is marked as an async function, the returned value will always be wrapped in a promise. Consider the following simple function:

function add(a, b) {
  return a + b;
}

We can simply mark the function as an async function by using the async keyword in the function definition:

async function add(a, b) {
  return a + b;
}

Now when we call the function we will get a promise that wraps the actual value. To access the actual value we use the then method and read the value through the callback argument:

const result = add(1, 2);
result.then(function(sum) {
  console.log(sum);
});

Await Operator

The await operator can only be used inside an async function. When you place the await operator behind an asynchronous operation, the execution is "paused" until the result is available. Let's say that we want to read the content of a file and wait until we have the content. Then we want to write the content to another file only after the read operation has completed. To do that we can define an async function readWrite that awaits on each task in the body of the function:

code/async-await/read-write-file.js

async function readWrite() {
  const content = await readFile('./example.txt', 'utf-8');
  const result = await writeFile('./example-copy.txt', content);
  return result;
}

The readWrite function is marked async which means we can use the await operator in the function body. On line 1 we wait until reading the content of the file is finished. And then on line 2 we write to the file and then wait until it's finished. On line 3 we simply return the result of the write operation.

Now let's look at another example involving a couple of async tasks that depend on each other. Below is the summary of the order of the operations to execute:

  1. Make a GET request to a public endpoint to get a post object
  2. List the content of a local directory and pick the file that ends with the .txt extension.
  3. Read the content of this file and and append it to the body of the post obtained from step 1
  4. Write the result to a file locally called final.txt

In the snippet below you can see how each async operation is accompanied with the await operator pausing the main function to wait until the result is available:

code/async-await/post-body-example/main.js

async function main() {
  const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
  const postBody = response.data.body;
  const localFolderList = await readdir('.');
  const textFiles = localFolderList.filter(onlyTextFiles);
  const localFileContent = await readFile(textFiles[0], 'utf-8');
  const finalResult = localFileContent + postBody;
  const writeResult = await writeFile('./final.txt', finalResult);
  return writeResult;
}

The code above looks synchronous, but it's actually not. Nothing else is blocked while each task is being performed. That's an important thing to remember, asynchronous code don't blocks other operations that are scheduled to be executed, resulting in a more efficient execution. Now let's take the example above one step further. Let's ask ourselves: what are the tasks that can be executed separately, and what are the tasks that absolutely need to be run in a certain order. In other words, how can we split this operation into two or more async parts? One possible option might be to split it up into two operations:

  1. Read folder content, get the text file, read the content of the text file
  2. Make GET request to get a post object

If you think about it, you don't need any information from the http call to read the content of the local text file. However, to read the content of the local text file, you need to first get the folder content and filter the text files. Now that we have grouped the tasks, we can use the Promise.all pattern that we used in the Promises chapter to perform all the tasks:

code/async-await/post-body-example/main-group.js

async function readLocalContent() {
  const localFolderList = await readdir('.');
  const textFiles = localFolderList.filter(onlyTextFiles);
  const localFileContent = await readFile(textFiles[0], 'utf-8');
  return localFileContent;
}

async function getPostObject() {
  const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
  return response.data.body;
}

async function main() {
  try {
    const results = await Promise.all([readLocalContent(), getPostObject()]);
    const finalContent = results[0] + results[1];
    return writeFile('./final2.txt', finalContent);
  } catch(e) {
    return Promise.reject(e);
  }
}

In the snippet above we have split up the two tasks into separate async functions allowing us to effectively run the two groups of tasks concurrently. In the main function we have used Promise.all to wait for both of the operations to finish and use the result to write the final content to a file.

Basic Error Handling

The interesting thing about async functions is that you can simply use a try catch block around a piece of asynchronous code and catch errors:

code/async-await/read-write-file-catch-error.js

async function readWrite() {
  try {
    const content = await readFile('./example.txt', 'utf-8');
    const result = await writeFile('./example-copy.txt', content);
    return result;
  } catch (error) {
    console.log('An error happened while copying the file.');
    return Promise.reject(error);
  }
}

In the snippet above we wrap our async code with a try catch block. If an error happens in any of the steps, we can catch it and return a rejected promise. In later chapters we'll dive deeper into error handling but for now it suffices to say that for most cases you can use a try catch block to handle errors inside an async function.

Async/Await Inside Loops

Let's say you need to perform a set of operations on a multiple local files. Your initial intuition might be use to use a looping mechanism like the forEach method:

const files = ['./a.txt', './b.txt', './c.txt'];
files.forEach(file => {
  const r1 = await task1(file);
  const r2 = await task2(r1);
});

There are two issues with the code above:

  1. The anonymous function passed to forEach is not marked async, so we can't use the await operator
  2. Even if we made the anonymous function async, the forEach method wouldn't wait for all the tasks to be finished

Instead of the forEach method, we have two options that would work:

  1. c-style for loop
async function main() {
  const files = ['./a.txt', './b.txt', './c.txt'];
  for (let i = 0; i<files.length; i++) {
    const r1 = await task1(files[i]);
    const r2 = await task2(r1);
  }
  return 'done';
}
  1. for of loop
async function main() {
  const files = ['./a.txt', './b.txt', './c.txt'];
  for (const file of files) {
    const r1 = await task1(file);
    const r2 = await task2(r1);
  }
  return 'done';
}

The downside of the above approaches is that we won't be able to run the tasks concurrently, even though each tasks is asynchronous. For example, if the first file is huge, but the last file is very small, the function is going to be paused until all the operations on the first file is finished. A more efficient approach would be to define each operation as a promise and then process them all using the Promise.all method:

const files = ['./a.txt', './b.txt', './c.txt'];

async function operation(f) {
  const r1 = await task1(f);
  const r2 = await task2(r1);
  return r2;
}

const tasksPromises = files.map(operation);

Promise.all(tasksPromises)
  .then(r => console.log(r))
  .catch(e => console.log(e))
  .then(_ => console.log('all done'));

In the snippet above, we define an async function that performs the two tasks in the order that we like. Then we make an array of promises using the map method. Finally, we process the promises using the Promise.all method. Note that the promises will be resolved in different order, but the tasks in the operation function will be done in the order that we wanted. You can take a look at an actual example in the code folder of the book's repo at code/async-await/loop/main.js.

results matching ""

    No results matching ""