Thursday, 29 June, 2017 UTC


Summary

The 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/await 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.
Please Note: This post is part of a series on working with database connections using various asynchronous patterns. See that post for more details and links to other options.

Promsie 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.
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 uses setTimeout 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 promises’ 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/await 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.
{
  "name": "promises",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Dan McGhan <[email protected]> (https://jsao.io/)",
  "license": "ISC",
  "dependencies": {
    "oracledb": "^1.13.1"
  }
}
This is a very basic package.json file. The only external dependency is oracledb.
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 drivers promise API 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.
module.exports = {
  user: 'hr',
  password: 'oracle',
  connectString: '192.168.56.101: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.
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 asychronously 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/await functions, which will cover in the next and final part of this series.