Subscribe

Share

Application Development

Keep Your Node.js Promises

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

By Dan McGhan

November/December 2017

In the previous article in this series,Using the Callback Pattern and the Async Module”, I covered using Node.js-style callbacks and the third-party Async module to execute a SQL query. While those patterns are effective, they are not the only ways of writing asynchronous code with Node.js. The Promise object, which became available with Node.js v0.12, provides a native means of reasoning about the success or failure of asynchronous operations.

It’s important to learn about promise objects (promises) because many modules use them in their APIs. Also, they are integral to asynchronous functions, which I’ll cover in the next part of this series. This article will cover the basics of promises and demonstrate how to use them to construct asynchronous applications. The last section, “Executing a Query with Promises,” will show how to use promises to execute a SQL query.

To set up a test environment you can use to work though this article’s examples, check out “Creating a Sandbox for Learning Node.js and Oracle Database” on the JavaScript and Oracle blog.

Creating Promises

The Promise object in JavaScript is a constructor function that returns new promise instances. Each promise instance has two important properties: state and value. When a new promise is created, the constructor function accepts a “resolver” function with two formal parameters: resolve and reject. These parameters will become functions that can transition the state of the promise instance and provide a value. Promises start off in the pending state, and they are typically resolved when the resolver successfully finishes or rejected if an error occurs.

Here’s an example demo script that shows several promises with different states and values resulting from the use of the resolve and reject functions.

const promise1 = new Promise(function(resolve, reject) {
  // noop
});

console.log(promise1);

const promise2 = new Promise(function(resolve, reject) {
  resolve('woohoo!');
});

console.log(promise2);

const promise3 = new Promise(function(resolve, reject) {
  reject(new Error('ouch!'));
});

console.log(promise3);

If you run the script above in either a web browser or Node.js, you should see output similar to the following. I used Chrome and then modified the output to fit better on the page. For now, ignore any errors related to uncaught or unhandled promise rejections. (There’s more on uncaught or unhandled promise rejections later.)

Promise {[[Status]]: "pending", [[Value]]: undefined}
Promise {[[Status]]: "resolved", [[Value]]: "woohoo!"}
Promise {[[Status]]: "rejected", [[Value]]: Error: ouch! at …}

As you can see, the first promise has a status of “pending” and no value, because neither the resolve nor reject function was used. The second promise, which was resolved with the resolve function, is in the “resolved” state, and its value is “woohoo!”—the value passed to the resolve function. The last promise’s state is “rejected” because it was rejected with the reject function, and its value is the error that was passed to reject.

Note that this demo script is completely synchronous. Typically, promises are resolved or rejected depending on the result of invoking one or more asynchronous APIs. Once a promise has been resolved or rejected, its state and value become immutable.

Responding to State Changes

To specify what should happen when the state of a promise 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, although 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 when the resolve or reject function is invoked in the resolver. The value passed through the reject function should always be an instance of Error, but that’s not enforced.

Here’s an example script 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: This section of code defines a function named getRandomNumber, which returns a new promise instance. The resolver function uses setTimeout to simulate an asynchronous API call, which returns a random number. To demonstrate how error handling works, some random numbers will throw exceptions.
  • Line 16: The getRandomNumber function 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 error.

If you run this script several times, you should see success messages and the occasional error message. Did you notice how the catch call flows from the then call? That technique is called promise chaining, and I’ll discuss that in more detail next.

Promise Chaining

Calls to then and catch return new promise instances. Of course, these promises also have then and catch methods, which allow calls to be chained together as needed. Because these then- and catch-created 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 finishes without error, the promise will be resolved. If the function returns a value, 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, the promise will be rejected and the error will be passed to the next error handler in the chain.

Consider the following script:

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 standard try/catch');
    console.log(err);

    return 'woohoo!';
  })
  .then(function(value) {
    console.log('Cool, we can simulate try/catch/finally!');
    console.log(value);
  });

If you run this script with Node.js or in a browser, you should see output similar to the following (I used Node.js here):

Got a value! 42
Caught the error without standard try/catch
Error: Error on the main thread
    ...
Cool, we can simulate try/catch/finally!
woohoo!

As you can see, errors thrown and values returned are routed to the next appropriate handler in the chain. This fact allows Node.js developers 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 this 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 asynchronous operations in series without running into callback hell. But what about more-complex flows?

For running operations in parallel, the Promise constructor function includes the 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 and race waits only for the first to be resolved or rejected.

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 asynchronous functions: just use a loop!

Error Handling

I’ve already covered some error handling basics in the section on promise chaining. In this section, I want to explain something that often trips up folks who are new to promises.

Consider 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 standard try/catch');
    console.log(err);
  })
  .then(function() {
    console.log('Cool, we can simulate try/catch/finally!');

    throw new Error('Ouch, another error!');
  });

Running this updated script should give you output similar to the following:

Got a value! 42
Caught the error without standard try/catch
Error: Error on the main thread
    ...
Cool, we can simulate try/catch/finally!
(node:11780) UnhandledPromiseRejectionWarning: 
Unhandled promise rejection 
(rejection id: 1): Error: Ouch, another error!
(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 an error will be passed to the next error handler in the chain. But what happens if there are no more error handlers?

The updated 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, when code throws errors in 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 creates an unhandledRejection event on the process object. If there’s no handler for that event, you’ll see the UnhandledPromiseRejectionWarning text in the output. According to the deprecation warning, unhandled promise rejections will kill the process in the future.

The solution is simple enough: be sure to handle those rejections!

Executing a Query with Promises

Here’s a practical example that demonstrates how promises can be used to execute a query. As in my previous article, this is a three-step process that must be done serially.

To use promises to execute a query, first obtain a connection to the database, then use the connection to execute the query, and finally close the connection. Each step is an asynchronous operation that must include error handling logic.

All the asynchronous methods of the driver have been configured to return a promise if the last parameter passed in is not a callback function. This makes it easy to choose the pattern you prefer.

const oracledb = require('oracledb');
const dbConfig = {
  user: 'hr',
  password: 'oracle',
  connectString: 'localhost:1521/orcl'
};
let conn; // Declared here for scoping purposes

oracledb.getConnection(dbConfig)
  .then(function(c) {
    console.log('Connected to database');

    conn = c;

    return conn.execute(
      'select *
      from employees',
      [], // no binds
      {
        outFormat: oracledb.OBJECT
      }
    );
  })
  .then(result => {
    console.log('Query executed');
    console.log(result.rows);
  })
  .catch(err => {
    console.log('Error in processing', err);
  })
  .then(() => {
    if (conn) { // conn assignment worked, need to close
      return conn.close();
    }
  })
  .then(function() {
    console.log('Connection closed');
  })
  .catch(err => {
    console.log('Error closing connection', err);
  });

Copy the code block above to a new file, name the file promises.js, and run the script with Node.js. The output should match that of the previous script versions from my last article, written with Node.js-style callbacks and the Async module.

For many folks, this promise version of the script is easier to read than the previous versions. I believe that’s debatable and largely dependent on each reader’s understanding of promises. However, I think everyone will agree that the best option is the async function (async/await) pattern. I’ll cover that pattern in the next (and last) article in this series.

Next Steps

LEARN more about JavaScript and Oracle.

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