An introduction to error and exception handling in Node.js

An introduction to error and exception handling in Node.js

An exception is the occurrence of an abnormal condition in the flow of execution of a program. Exception handling is a fundamental requirement in developing with Node.js.

An exception is the occurrence of an abnormal condition in the flow of execution of a program. Exception handling is a fundamental requirement in developing with Node.js.

In Node.js, exceptions cause an application to terminate immediately. The most common causes of exceptions can be the following:

  1. Syntax errors.
  2. Reference errors(undefined or null).
  3. Errors raised by callback functions.
  4. Unhandled errors raised by Promises.
  5. HTTP errors.
  6. Errors raised by NPM modules.

Syntactic errors can be solved a priori by enabling syntactic verification on the IDE or on the development editor. These are the most immediate errors.

Reference errors occur or when a variable is unreachable from the current scope or when trying to access a property and method of an object that do not exist in the object in question.

To verify the existence of a property we can resort to the typeof operator.

if(typeof obj.property !== 'undefined') {
    //...
}

An alternative approach is to use the in operator.

if('property' in obj) {
    //...
}

Since JavaScript is a weakly typed language, check only the undefined value often it is not enough and it is necessary to specify the type of data we expect.

if(typeof post.id === 'number') {
    //...
}

null does not require the use of the typeof operator as it would return an inconsistent value (object) but more simply the identity operator.

if(post === null) {
    //...
}

A particular case of this type of error is very frequent when we accept user input. Suppose that the user enters a price in decimal format and our application must return a formatted string.

router.post('/price', (req, res, next) => {
    const { price } = req.body;
    const formattedPrice = parseFloat(price).toFixed(2);
    res.json({ formattedPrice });
});

If the user enters an empty string, parseFloat() will return NaN which in turn will generate an error in the toFixed () method as this method does not exist in NaN. The solution in these cases is to validate user input before using it for our operations.

The errors generated by the callback functions concern those methods of the core modules of Node.js or of the NPM modules that follow the error-first approach in the callback functions. In this approach, the first argument of the callback function will always be the error or the error object raised in the event of an exception.

const fs = require('fs');

fs.writeFile('test', 'Test', err => {
    if(err) {
        //...
    }
});

In this case, for example, the error can be raised if Node does not have sufficient permissions to write to the file system.

At this point we must introduce a fundamental construct of the JavaScript language which is actually little used by developers, that is try/catch/finally .

  1. try: this block evaluates the code and if it raises an error, the control passes to the catch block.
  2. catch: here we can handle the error object for example by informing the user.
  3. finally: this block will always be executed.

Let's see an example.

router.post('/price', (req, res, next) => {
    const { price } = req.body;
      let formattedPrice = '';
    try {
        formattedPrice = parseFloat(price).toFixed(2);
    } catch(err) {
        formattedPrice = 'N/A';
    } finally {
        formattedPrice += ' Euro';
    }
    res.json({ formattedPrice });
});

The documentation shows several examples of this construct. For the end user the key thing is to be informed about what went wrong while we can log the technical details internally.

This construct serves as an introduction to the problem of errors raised by the Promises. The Promises have this peculiarity: the error object of the catch block can be empty if in the try block the exception was not raised by a Promise with its reject callback but by a JavaScript expression. For example:

router.post('/products/price/:id', async (req, res, next) => {
   const { id } = req.params;
   try {
       const product = await products.findById(id);
       const formattedPrice = parseFloat(product.price).toFixed(2);
       res.json({ formattedPrice });
   } catch(err) {
       res.status(500);
       res.json({ err });
   }
});

In this case it may happen that the product does not exist but this does not raise an exception. Instead the error is created when trying to format the price. What will be returned from our endpoint it will not be the details of the error but an empty object.

HTTP errors can be raised by both Node core modules and NPM modules that handle HTTP requests. These errors can be either initializing or processing the request.

A typical initialization error is when we omit a request configuration parameter. Instead a processing error can occur for example when we are making a request using SSL / TLS and the remote server does not have a valid certificate.

The error in the core modules is handled by the request's error event.

const https = require('https');

https.get('https://api.test/posts', resp => {

}).on('error',  err => {
  console.log(err);
});

This asynchronous management allows us to make sure that the processing of the response by the server is affected by the presence or absence of HTTP errors. In this case indeed if the event takes place, the processing of the reply will be immediately prevented.

Finally, the NPM modules require careful study of their documentation, in particular of their error management, checking for example if the module in question is based on the Promise or the error-first approach in the callback functions. Depending on the approach used, a different strategy will have to be implemented.

For example, the validator module accepts only strings. Any other type of value will raise an error.