How to Interact With a Database Using Promises in Node.js
Understanding promises is essential to understanding async functions. Learn the basics of promises and demonstrate how they can be used to construct asynchronous apps.
Join the DZone community and get the full member experience.
Join For FreeThe first two patterns we looked at in this series were Node.js callbacks and the Async module. While effective, those patterns are not the only ways of writing asynchronous code. Patterns built around concepts such as “deferreds” and “promises” were first introduced to the JavaScript community via third-party libraries like Q and Bluebird. Over time a native implementation of promises was added to ECMAScript, then implemented in V8, and later integrated into Node.js.
Promises are important to learn because many modules use them in their APIs. Also, they are integral to async functions, which I’ll cover in the next part of the series. This post will cover the basics of promises and demonstrate how they can be used to construct asynchronous applications.
Promise Overview
Native support for promises was added to JavaScript via the Promise
object. The Promise
object is a constructor function that’s used to create new promise instances. Promise instances have two important properties: state and value. Let’s take a look at how we can manipulate those values and respond to state changes.
resolve
and reject
When a new promise is created, the constructor function accepts a “resolver” function that should have two formal parameters: resolve
and reject
. When the resolver function is executed, which happens immediately when creating new instances, these parameters will be functions that can transition the state of the promise and provide a value. Promises are typically resolved when the resolver completes successfully and rejected if an error occurs.
Here’s an example that shows several promise instances with different states and values:
const promise1 = new Promise(function(resolve, reject) {
// noop
});
console.log(promise1); // Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
const promise2 = new Promise(function(resolve, reject) {
resolve('foo');
});
console.log(promise2); // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "foo"}
const promise3 = new Promise(function(resolve, reject) {
reject(new Error('bar'));
});
console.log(promise3); // Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Error: bar at <anonymous>:14:10 at Promise (<anonymous>) at <anonymous>:13:28}
If you run the above script in either a browser or Node.js, you should see console output similar to the comments. For now, ignore any errors related to uncaught or unhandled promise rejections (more on that later).
Note that this demo script is completely synchronous. Promises usually start out in a pending
state and are resolved or rejected asynchronously. Once a promise has been resolved or rejected, its state and value become immutable.
then
and catch
To specify what should happen when the state of an instance changes, promises have two methods: then
and catch
. Both methods accept callback functions that may be invoked asynchronously at some point in the future.
The then
method is typically used to specify what should happen when the promise is resolved, though it can accept a second function to handle rejections too. The catch
method is explicitly used to handle rejections.
The callback functions passed to then
and catch
will receive the value passed through the when the resolve
or reject
functions are invoked in the resolver. The value passed through rejections should always be an instance of Error
, though that’s not enforced.
Here’s an example that uses resolve
and reject
asynchronously. The then
and catch
methods are used to define what should happen when the promise’s state changes.
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);
});
}
getRandomNumber()
.then(function(value) {
console.log('Async success!', value);
})
.catch(function(err) {
console.log('Caught an error!', err);
});
Here’s what’s going on in the script:
Lines 1-14: A function named
getRandomNumber
is defined.getRandomNumber
returns a new promise instance. The resolver function usessetTimeout
to simulate an async call which returns a random number. To demonstrate how error handling works, some random numbers will throw exceptions.Line: 16: The
getRandomNumber
is invoked and immediately returns a promise in the pending state.Lines 17-19: The
then
method of the promise is passed a callback that logs a success message with the resolved value.Lines 20-22: The
catch
method of the promise is passed a callback that logs an error message with the rejected value (Error instance).
If you run the script above several times, you should see success messages and the occasional error message. Did you notice how the catch call flows off the then call? That technique is called promise chaining.
Promise Chaining
Calls to then
and catch
return new promise instances. Of course, these instances also have then
and catch
methods, which allows calls to be chained together as needed. Becuase these promises are not created with the constructor function, they are not resolved or rejected with a resolver function. Instead, if the function passed into then
or catch
completes without error, then the promise will be resolved. If the function returns a value, then it will set the promise’s value and be passed to the next then
handler in the chain. If an error is thrown and goes unhandled, then the promise will be rejected and the error will be passed to the next error handler in the chain.
const myPromise = new Promise(function(resolve, reject) {
resolve(42);
});
myPromise
.then(function(value) {
console.log('Got a value!', value);
throw new Error('Error on the main thread');
})
.catch(function(err) {
console.log('Caught the error without try/catch above');
console.log(err);
return 'foo';
})
.then(function(value) {
console.log('Hey look, still going! We can simulate try...catch...finally!');
console.log(value);
});
If you run the above script with Node.js or in a browser, you should see output like the following (I used Node.js):
Got a value! 42
Caught the error without try/catch above
Error: Error on the main thread
...
Hey look, still going! We can simulate try...catch...finally!
foo
As you can see, errors thrown and values returned are routed to the next appropriate handler in the chain. This fact allows us to simulate a try…catch…finally
block.
Things get more interesting when the value returned is a promise. When this happens, the next handler in the chain will not be invoked until that promise is resolved or rejected.
Here’s an example:
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);
});
}
getRandomNumber()
.then(function(value) {
console.log('Value 1:', value);
return getRandomNumber();
})
.then(function(value) {
console.log('Value 2:', value);
return getRandomNumber();
})
.then(function(value) {
console.log('Value 3:', value);
})
.catch(function(err) {
console.log('Caught an error!', err);
});
With a little luck, when you run the script, you should see three random values printed to the console every two seconds. If an “error” occurs at any point in the chain, the remaining then
functions will be skipped and the error will be passed to the next error handler in the chain. As you can see, promise chaining is a great way to run async operations in series without running into callback hell. But what about more complex flows?
For running operations in parallel, the Promise
constructor function has all
and race
methods. These methods return promises that are resolved or rejected a little differently: all
waits for all the promises passed in to be resolved or rejected while race
only waits for the first.
Asynchronous iteration of collections, something that’s quite trivial with Async, is not so easy with promises. I’m not showing the technique here because there’s a much simpler way to do this now with async functions — just use a loop!
Error Handling
We’ve already covered some of the basics regarding error handling in the section on promise chaining. In this section, I want to explain something that often trips up folks that are new to promises.
Have a look at this example:
const myPromise = new Promise(function(resolve, reject) {
resolve(42);
});
myPromise
.then(function(value) {
console.log('Got a value!', value);
throw new Error('Error on the main thread');
})
.catch(function(err) {
console.log('Caught the error without try/catch above');
console.log(err);
})
.then(function() {
console.log('Hey look, still going! We can simulate try...catch...finally!');
throw new Error('Opps, bet you didn\'t see this error coming!');
});
Running the script above should give you output like the following:
Got a value! 42
Caught the error without try/catch above
Error: Error on the main thread
...
Hey look, still going! We can simulate try...catch...finally!
(node:11780) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Opps, bet you didn't see this error coming!
(node:11780) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Note that unhandled errors thrown in functions passed to then and catch
are swallowed up and treated like rejections. This means the error will be passed to the next error handler in the chain. But what happens if there are no more error handlers?
The demo script above throws two errors on the main thread. The first error is handled properly by the subsequent catch
handler. However, there are no error handlers after the second error is thrown. This resulted in an unhandled rejection and the warnings in the console.
Typically in Node.js, when code throws errors on the main thread outside of a try/catch block, the process is killed. In the case of an unhandled rejection in a promise chain, Node.js emits an unhandledRejection
event on the process object. If there’s no handler for that event, then you’ll get the UnhandledPromiseRejectionWarning
seen above. According to the DeprecationWarning
, unhandled promise rejections will kill the process in the future.
The solution is simple enough: be sure to handle those rejections! Check out this excellent post by Valeri Karpov to learn more about unhandled rejections.
Promise Demo App
The promise demo app is comprised of the following four files. The files are also available via this Gist.
package.json
:
{
"name": "promises",
"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');
oracledb.createPool(dbConfig)
.then(function() {
return employees.getEmployee(101);
})
.then(function(emp) {
console.log(emp);
})
.catch(function(err) {
console.log(err);
});
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
leverages the driver’s promise APIs to create a connection pool and start a promise chain. Although the pool is passed to the then handler for 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(function(resolve, reject) {
let conn; // Declared here for scoping purposes.
oracledb
.getConnection()
.then(function(c) {
console.log('Connected to database');
conn = c;
return conn.execute(
`select *
from employees
where employee_id = :emp_id`,
[empId],
{
outFormat: oracledb.OBJECT
}
);
})
.then(
function(result) {
console.log('Query executed');
resolve(result.rows[0]);
},
function(err) {
console.log('Error occurred', err);
reject(err);
}
)
.then(function() {
if (conn) {
// If conn assignment worked, need to close.
return conn.close();
}
})
.then(function() {
console.log('Connection closed');
})
.catch(function(err) {
// If error during close, just log.
console.log('Error closing connection', e);
});
});
}
module.exports.getEmployee = getEmployee;
In this version of the employees module, the getEmployee
function was written as a promise-based API – it immediately returns a new promise instance which is asynchronously resolved or rejected. Internally, the function uses the driver’s promise based APIs to get a connection to the database, use it to execute a query, and then close a connection.
The promise chain is written in a way that simulates a try…catch…finally
block. The final catch
is there just in case there’s an error closing the connection (which would be rare). One could use the unhandledRejection
event as an alternative.
I hope this post has helped you to understand how promises work in Node.js. Remember that understanding promises is essential to understanding async functions, which I’ll cover in the next and final part of this series.
Published at DZone with permission of Dan McGhan, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments