Subscribe

Share

Application Development

Using Async Functions

Here’s Part 4 in a four-part series on asynchronous Node.js development.

By Dan McGhan

January/February 2018

So far in this asynchronous Node.js series, I’ve covered Node.js-style callbacks, the Async module, and promises. In this final part of the series, I’ll teach you about async functions (aka async/await). To me, async functions are the most exciting thing to happen to JavaScript since Ajax, because—finally—you can read JavaScript code in a synchronous manner while it executes asynchronously as it always has.

Async Functions Overview

Async functions are a relatively new feature of JavaScript (and not specific to Node.js). Support for the feature first landed in Node.js v7.6 via an update to the V8 JavaScript engine. Because async functions rely heavily on promises, I recommend you read the previous article if you’re not familiar with them.

I like to think of async functions as two parts: async and await. Let’s look at each part in turn.

async. For the longest time, JavaScript developers could create functions using function statements (which must be named) or function expressions (which are often anonymous).

function getNumber() { // Function statement
  return 42;
}

let logNumber = function() { // Function expression
  console.log(getNumber());
}

logNumber(); // 42

If you run this script in Node.js, you should see 42 printed to the console.

JavaScript now has asynchronous counterparts to these constructs. Placing the new async keyword before the function statement or expression returns an AsyncFunction (async function) object.

async function getNumber() { // Async function statement
  return 42;
}

let logNumber = async function() { // Async function expression
  console.log(getNumber());
}

logNumber(); // Promise { 42 }

If you run this script in Node.js, you should see Promise { 42 } printed to the console. As you can see, when async functions are invoked, they return promises rather than the actual values.

For the async version of the script to be the functional equivalent of the first example, I’d have to rewrite it as follows.

async function getNumber() { // Async function statement
  return 42;
}

let logNumber = async function() { // Async function expression
  getNumber() // returns a promise
    .then(function(value) {
      console.log(value);
    });
}

logNumber(); // 42

With this script, I’m back to logging 42 (rather than Promise { 42 }).

Just as you saw with promise chaining, if the async function is completed without error, the promise it returns is resolved. If the function returns a value, that becomes the promise’s value. If an error is thrown and goes unhandled, the promise is rejected and the error becomes the promise’s value.

Though interesting, returning promises isn’t what makes async functions special. You could, after all, return promises from regular functions. What makes async functions special is await.

await. The await operator, which is available only inside an async function, is as close to magic as you’ll get in JavaScript. It’s like hitting the pause button on your code so that it can wait for a promise to be resolved or rejected before continuing. This is a concept known as a coroutine. Coroutines have been available in JavaScript since generator functions were introduced, but async functions make them much more approachable.

Using await will not block the main thread. Instead, the currently running call stack, up to the point of await, will be completed so that other functions in the callback queue can be executed. When the promise is resolved or rejected, the remaining portion of the code is queued for execution. If the promise was resolved, its value is returned. If the promise was rejected, the rejected value is thrown on the main thread.

Here’s a demonstration of await that uses setTimeout to simulate an async API. I’ve added some additional console output to help illustrate what’s happening.

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
 
      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumber() {
  let number;

  console.log('before await', number);

  number = await getRandomNumber();

  console.log('after await', number);
}

console.log('before async call');

logNumber();

console.log('after async call');

When I run this script in Node.js without an error occurring, the output will appear as follows. (I’ve added a comment where the two-second delay happens.)

before async call
before await undefined
after async call
# 2 second delay
after await 0.22454453163016597

Note that “after async call” was logged before “after await 0.22454453163016597.” Only the remaining code in the async function is paused; the remaining synchronous code in the call stack will finish executing.

If an error is thrown, you’ll see UnhandledPromiseRejectionWarning, which I covered in the last article. You could handle the rejection with the methods mentioned in that article or by using try/catch.

Try/catch

In the first article in this series, I explained why try/catch blocks don’t work with asynchronous operations—you can’t catch errors that occur outside of the current call stack. But now with async functions, you can use try/catch for asynchronous operations.

Here’s a slightly modified version of the previous script that catches errors that occur in the async API and uses a default value instead.

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
 
      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumber() {
  let number;

  try {
    number = await getRandomNumber();
  } catch (err) {
    number = 42;
  }

  console.log(number);
}

logNumber();

If you run that script enough times, you’ll eventually get 42 in the output. So, try/catch blocks finally work with async operations. Woohoo!

Async Loops

In addition to being able to use try/catch blocks again, you can use asynchronous loops, too. In the following example, I use a simple for loop that prints three values serially.

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
 
      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  }); 
}

async function logNumbers() {
  for (let x = 0; x < 3; x += 1) {
    console.log(await getRandomNumber());
  }
}

logNumbers();

Running this script in Node.js, you should see three numbers printed to the console every two seconds. There are no third-party libraries and no complicated promise chains, just a simple loop. Loops work again!

Parallel Execution

Clearly, async functions make it easy to do sequential flows and use standard JavaScript constructs with asynchronous operations. But what about parallel flows? This is where Promise.all and Promise.race come in handy. Because they both return promises, await can work with them as with any other promise-based API.

Here’s an example that uses Promise.all to get three random numbers in parallel.

function getRandomNumber() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const randomValue = Math.random();
      const error = randomValue > .8 ? true : false;
 
      if (error) {
        reject(new Error('Ooops, something broke!'));
      } else {
        resolve(randomValue);
      }
    }, 2000);
  });
}
 
async function logNumbers() {
  let promises = [];
 
  promises[0] = getRandomNumber();
  promises[1] = getRandomNumber();
  promises[2] = getRandomNumber();
 
  try {
    values = await Promise.all(promises);
    console.log(values);
  } catch (err) {
    console.log(err);
  }
}
 
logNumbers();

Because Promise.all rejects its promise if any promise passed in is rejected, you might need to run the script a few times to see the three random numbers printed out.

Executing a Query with Async Functions

I’ll end this article with one last example of executing a query using the Node.js driver for Oracle Database—only this time using an async function. For brevity, I’m using the async version of an immediately invoked function expression (IIFE).

const oracledb = require('oracledb');
const dbConfig = {
  user: 'hr',
  password: 'oracle',
  connectString: 'localhost:1521/orcl'
};

(async function() {
  let conn; // Declared here for scoping purposes

  try {
    conn = await oracledb.getConnection(dbConfig);

    console.log('Connected to database');

    let result = await conn.execute(
      'select *
      from employees',
      [], // no binds
      {
        outFormat: oracledb.OBJECT
      }
    );

    console.log('Query executed');
    console.log(result.rows);
  } catch (err) {
    console.log('Error in processing', err);
  } finally {
    if (conn) { // conn assignment worked, need to close
      try {
        await conn.close();

        console.log('Connection closed');
      } catch (err) {
        console.log('Error closing connection', err);
      }
    }
  }
})();

Because the driver’s asynchronous APIs already support promises, you can use the async operator without any additional work. To me, this version of the query example is the simplest, and it doesn’t hurt that it uses the fewest lines of code as well.

I hope you now have a better grasp of async functions and are as excited as I am about using them!

Next Steps

LEARN more about JavaScript and Oracle.

Photography by Carey Kirkella/The Verbatim Agency, iStock.com/Pixtum