Error Handling

In this section of the document, we cover the best practices for error handling in JavaScript. Like with any robust application, error handling is essential.

The first part consists of an overview of how JavaScript handles errors. In the second part, we will provide code examples for different types of error handling in JavaScript.

Overview

JavaScript has an Error constructor that creates an error object. Per usual, when a runtime exception occurs, an error is thrown. The syntax is the following:

new Error([message[, filename[, lineNumber]]])

The resulting error object can also be used for user-defined exception.

An important distinction to keep in mind is related to the nature of the error. Errors can be the result of Operational Errors or Programmer errors.

Operational errors are errors related to operations which might fail. This means that the application logic is correct, but an unexpected error occurred. Examples of this include Input/Output (I/O) errors, network problems, out of memory, etc.

The other types of errors are called Programmer errors, and these are related to logic bugs in the application's code. These errors are a result of a problem in the application's code. Yet despite being able to correct the issue by changing the code, they can't be handled properly since the problem stems from mistakes in the code. Examples of this include accessing properties of an "undefined" variable, type confusion (passing to a function an unexpected data type), etc.

In Node.js, there are three main ways to deliver errors:

  • throw the error (exception)
  • Passing the error to a callback(), a function used to handle errors and the results of asynchronous operations
  • Using an EventEmitter to emit an error event

Also, it is important to understand the difference between error and exception. Errors can be constructed then passed directly to another function or thrown. If an error is thrown, it becomes an exception.

In plain synchronous JavaScript, the syntax of an exception tends to be the following:

throw new Error('Some error.');

Throwing exceptions is not a common pattern of JavaScript or Node.js's core. Instead, most native APIs pass Error as an argument to the callback function

callback(new Error('Some error'));

The usage of callback() is due to the asynchronous nature of JavaScript and the associated errors that can occur.

In synchronous operations like I/O errors, all exceptions should be caught not only due to good practices, but also to ensure that if the application fails, it fails on a controlled fashion.

As an example, consider this incorrect way to read a file:

const fs = require('fs');

const content = fs.readFileSync('/tmp/missing-file.txt');

Notice that there is no error catching. By ignoring errors, if the application fails, there's no way to ensure it failed in a controlled way, since we have no way of knowing if anything went wrong.

So be careful and always catch exceptions in synchronous operations.

The correct way to write the previous code in order to catch exceptions is:

const fs = require('fs');

try {
  const content = fs.readFileSync('/tmp/missing-file.txt');
} catch (e) {
  console.error('ups... something went wrong');
}

ES6 introduced promises. The simplest way to look at promises is to see them as "a proxy for a value not necessarily known when the promise is created."

What this means is that by using promises, there are several advantages for the developer.

Mainly:

  • No more callback pyramids aka "callback hell"
  • No more error handling every second line
  • No more reliance on external libraries for simple operations like getting the result of a loop

IMPORTANT NOTE: Not everything is good news, one of the biggest caveats that developers have to keep in mind when using promises is that any exception thrown within a then handler, a catch handler or within the function passed to new Promise, will be silently disposed unless handled manually.

Consider the following code:

function validateUser () {
  return new Promise ((resolve, reject) => {
    getUser().then(user => {
      getUserPassword(user).then(userpassword => {
        checkPassword(userpassword).then(validated => {
          resolve(validated)
        });
      });
    });
  });
}

By using promises, we can re-write our previous code in the following manner. Please note that we are still not handling errors:

function validateUser () {
  return getUser()
    .then(getUserPassword)
    .then(checkPassword)
}

Now the same code, using promises and handling errors:

function validateUser () {
  return getUser()
    .then(getUserPassword)
    .then(checkPassword)
    .catch(fallbackForRequestFail); // error handling
}

ES8/ES2017 introduced the async/await, which allows developers to write asynchronous code in a synchronous fashion

app.post('/login', (req, res, next) => {
  const user = req.body.user;
  const password = req.body.password;

  // input validation omitted for brevity

  db.Users.find({user: user, password: password}, (err, user_acc) => {
    if (err) {
      logger.error(err);
      return res.status(400).send('Login failed');
    }

    user_acc.getUserDetails(user_acc, (err, user_acc) => {
        if (err) {
          logger.error(err);
          return res.status(400).send('Login failed');
        }

        user_acc.userLoginLog(user_acc, (err) => {
          if (err) {
            logger.error(err);
            return res.status(400).send('Login failed');
          }

          res.send(user_acc);
        });
    });
  });
});

Can now be written as:

app.post('/login', async (req, res, next) => {
  const user = req.body.user;
  const password = req.body.password;

  try {
    const user_acc = await db.Users.find({user: user, password: password});
    const user_acc.details = await user_acc.getUserDetails(user_acc);

    await user_acc.userLoginLog(user_acc);

    res.send(user_acc);
  } catch (err) {
    logger.error(err);
    return res.status(400).send('Login failed');
  }
});

It is also good practice to implement custom error messages or custom error pages as a way to make sure that no information is leaked when an error occurs.

The following examples implement a custom error page for web applications based in Express Node.js web application framework

const express = require('express')
const app = express();

app.enable('verbose errors');
app.use(app.router);

app.use((req, res, next) => {
  res.status(404);

  // respond with html page
  if (req.accepts('html')) {
    res.render('404', { url: req.url });
    return;
  }

  // respond with json
  if (req.accepts('json')) {
    res.send({ error: 'Not found' });
    return;
  }

  // default to plain-text. send()
  res.type('txt').send('Not found');

  // Routes
});

The final way in which we will demonstrate to capture errors is by using EventEmitter.

If you're using event oriented programming or code that deals with streams, an error handler should be present. EventEmitters fire an error event that can be captured. The following is an example of EventEmitter using promises:

const EventEmitter = require('events');

class Emitter extends EventEmitter {}

const myEmitter = new Emitter();
const logger = console;

myEmitter.on('error', (err) => {
  logger.error('Unexpected error.');
});

myEmitter.emit('error', new Error('Something went wrong.'));

// In case of an unhandled exception, terminate gracefully.
// Using promises.
process.on('uncaughtException', (error, promise) => {
  logger.error('Uncaught exception!', { error: error, promise: promise });
  process.exit(1);
});

Another important detail to keep in mind is to guarantee that no sensitive information is within the error responses. This includes no system details, session identifiers, account information, stack traces or debug information.

Finally, it is necessary to ensure that in case of an error associated with the security controls, by default, access is denied.

results matching ""

    No results matching ""