How to Interact With a Database Using Async Functions in Node.js
In the final post of the series on interacting with databases, we'll learn about async functions — the most exciting thing to happen to JavaScript since Ajax.
Join the DZone community and get the full member experience.
Join For FreeSo far in this async series, we’ve covered Node.js style callbacks, the Async module, and promises. In this final part of the series, we’ll learn about async functions (AKA async/await). To me, async functions are the most exciting thing to happen to JavaScript since Ajax. Finally, we 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 (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 post before continuing.
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, we’ve had the ability to create functions in JavaScript using function statements (must be named) or function expressions (often anonymous).
function getNumber() { // Function statment
return 42;
}
let logNumber = function() { // Function expression
console.log(getNumber());
}
logNumber(); // 42
If you run the script above 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 statment
return 42;
}
let logNumber = async function() { // Async function expression
console.log(getNumber());
}
logNumber(); // Promise { 42 }
Running this script in Node.js should print Promise { 42 }
. As you can see, when async functions are invoked, they return promises rather than the actual values returned!
For the async-based script to be the functional equivalent of the first, we’d have to rewrite it as follows.
async function getNumber() { // Async function statment
return 42;
}
let logNumber = async function() { // Async function expression
getNumber() // returns a promise
.then(function(value) {
console.log(value);
});
}
logNumber(); // 42
Now we’re back to logging the value 42
.
Just as we saw with promise chaining in the previous post, if the async function completes without error, then the promise it returns is resolved. If the function returns a value, then that becomes the promise’s value. If an error is thrown and goes unhandled, then the promise is rejected and the error becomes the promise’s value.
Though interesting, returning promises isn’t what makes async functions special. We could, after all, just return promises from regular functions. What makes async functions special is await
.
await
The await
operator, which is only available inside of an async function, is where the magic happens. 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.
Await does not block the main thread. Instead, the currently running call stack, up to the point of await
, is allowed to finish 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 this script is run in Node.js without an error occurring, the output will look like the following (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 call stack will finish.
If an error is thrown, you’ll see the UnhandledPromiseRejectionWarning
we covered in the last post. The rejection could be handled with the methods mentioned in that post or using try…catch
!
try…catch
In the first post 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 that we have async functions, try…catch
can be used for asynchronous operations!
Here’s a stripped down 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. try…catch
works again, woohoo!
Async Loops
In addition to being able to use try…catch
blocks again, we can do asynchronous loops too! In the following example, I use a simple for
loop that logs out 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. No third party libraries, no complicated promise chains, just a simple loop. Loops work again, yay!
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 like 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();
Promise.all(promises)
.then(function(values) {
console.log(values);
})
.catch(function(err) {
console.log(err);
});
}
logNumbers();
Because Promise.all
rejects its promise if any promise passed in is rejected, you may need to run the script a few times to see the three random numbers printed out.
Async Function Demo App
The async function demo app is comprised of the following four files. The files are also available via this Gist.
package.json
:
{
"name": "async-functions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Dan McGhan <dan.mcghan@oracle.com> (https://jsao.io/)",
"license": "ISC",
"dependencies": {
"oracledb": "^1.13.1"
}
}
This is a very basic package.json
file. The only external dependency is oracledb.
index.js
:
const oracledb = require('oracledb');
const dbConfig = require('./db-config.js');
const employees = require('./employees.js');
async function startApp() {
try {
await oracledb.createPool(dbConfig);
let emp = await employees.getEmployee(101);
console.log(emp);
} catch (err) {
console.log('Opps, an error occurred', err);
}
}
startApp();
All of the async methods in node-oracledb are overloaded to work with callback functions or promises. If a callback function is not passed in as the last parameter, then a promise will be returned. This version of the index.js
uses the await
operator with the driver’s promise APIs to create a connection pool and fetch an employee. Although the pool is returned from the call to createPool
, it’s not referenced here as the built-in pool cache will be used in employees.js
.
db-config.js
:
module.exports = {
user: 'hr',
password: 'oracle',
connectString: 'localhost:1521/orcl',
poolMax: 20,
poolMin: 20,
poolIncrement: 0
};
The db-config.js
file is used in index.js
to provide the connection info for the database. This configuration should work with the DB App Dev VM, but it will need to be adjusted for other environments.
employees.js
:
const oracledb = require('oracledb');
function getEmployee(empId) {
return new Promise(async function(resolve, reject) {
let conn; // Declared here for scoping purposes.
try {
conn = await oracledb.getConnection();
console.log('Connected to database');
let result = await conn.execute(
`select *
from employees
where employee_id = :emp_id`,
[empId],
{
outFormat: oracledb.OBJECT
}
);
console.log('Query executed');
resolve(result.rows[0]);
} catch (err) {
console.log('Error occurred', err);
reject(err);
} finally {
// If conn assignment worked, need to close.
if (conn) {
try {
await conn.close();
console.log('Connection closed');
} catch (err) {
console.log('Error closing connection', err);
}
}
}
});
}
module.exports.getEmployee = getEmployee;
This version of the employees module is similar to the promise version in that the getEmployee
function was written as a promise-based API — it immediately returns a new promise instance that is asynchronously resolved or rejected. The main difference is that await
is used with the driver’s promise APIs to get a connection to the database, use it to execute a query, and then close a connection.
A try…catch…finally
block was used to catch errors and ensure the connection was closed either way. To me, this version of the module is the simplest to read of all those in the series and it doesn’t hurt that it has the fewest lines of code as well.
Hopefully, you now have a better grasp on async functions and are as excited as I am about using them!
Published at DZone with permission of Dan McGhan, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments