Kedar Kodgire Kedar is a tech blogger, UI/UX designer and developer, cloud engineer, and machine-learning enthusiast. He's passionate about exploring new technologies and sharing them with everyone.

Error handling in Node.js

9 min read 2584

Error Handling In Node.js

Errors are part of a developer’s life. We can neither run nor hide from them. While building production-ready software, we need to manage errors effectively to:

  1. improve the end-user experience; i.e., providing correct information and not the generic message “Unable to fulfill the request”
  2. develop a robust codebase
  3. recede development time by finding bugs efficiently
  4. avoid abruptly stopping a program

Because you are here, I assume you are probably a web developer with a JavaScript background. Let’s take the typical use case of reading a file in Node.js without handling an error:

var fs = require('fs')

# read a file
const data = fs.readFileSync('/Users/Kedar/node.txt')

console.log("an important piece of code that should be run at the end")

Note that Node.js should execute some critical piece of code after the file-reading task. When we run it, we receive the output as shown below:

Output:

$node main.js
fs.js:641
  return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
                 ^

Error: ENOENT: no such file or directory, open '/Users/Kedar/node.txt'
    at Error (native)
    at Object.fs.openSync (fs.js:641:18)
    at Object.fs.readFileSync (fs.js:509:33)
    at Object.<anonymous> (/home/cg/root/7717036/main.js:3:17)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)

Here, the program ends abruptly without executing the necessary code. We will discuss the revised code with error handling later in the try...catch blocks section. This example demonstrates only one of many issues faced without error handling. Let’s take a look at what we’ll cover to better understand how we can handle errors:

Before we learn about error handling, let’s understand an Error in Node.js.

Error

Error is an extension of the Error object in Javascript. The error can be constructed and thrown or passed to some function. Let’s check out some examples:

throw new Error('bad request'); // throwing new error
callback_function(new Error('connectivity issue')); // passing error as an argument

While creating an error, we need to pass a human-readable string as an argument to understand what went wrong when our program is working incorrectly. In other words, we are creating an object by passing the string to the Error constructor.

You also need to know that errors and exceptions are different in JavaScript, particularly Node.js. The errors are the instances of an Error class, and when you throw an error, it becomes an exception.

Humans do not cause all errors. There are two types of errors, programmer and operational. We use the phrase “error” to describe both, but they are quite different in reality because of their root causes. Let’s have a look at each one.

Programmer errors

Programmer errors depict issues with the program written — bugs. In other words, these are the errors caused by the programmer’s mistakes while writing a program. We cannot handle these errors properly, and the only way to correct them is to fix the codebase. Here are some of the common programmer errors:

  • Array index out of bounds — trying to access the seventh element of the array when only six are available
  • Syntax errors — failing to close the curly braces while defining a JavaScript function
  • Reference errors — accessing a function or variables that are not defined
  • Deprecation errors and warnings — calling an asynchronous function without a callback
  • Type error — x object is not iterable
  • Failing to handle operational errors

Operational errors

Every program faces operational errors (even if the program is correct). These are issues during runtime due to external factors that can interrupt the program’s normal flow. Unlike programmer errors, we can understand and handle them. These are some examples:

  • Unable to connect server/database
  • Request timeout
  • Invalid input from the user
  • Socket hang-up
  • 500 response from a server
  • File not found

You might wonder why this segregation is necessary because both cause the same effect, interrupting the program? Well, you might have to act based on the type of error. For example, restarting the app may not be a suitable action for file not found error (operational error) but restarting might be helpful when your program is failing to catch the rejected promise (programmer error).

Now that you know about the errors, let’s handle them. We can avoid the abrupt termination of our program by managing these errors, which is an essential part of production-ready code.

Error handling techniques

To handle the errors effectively, we need to understand the error delivery techniques.

There are four fundamental strategies to report errors in Node.js:

  1. try…catch blocks
  2. Callbacks
  3. Promises
  4. Event emitters

Let’s understand using them one by one.

try…catch blocks

In the try…catch method, the try block surrounds the code where the error can occur. In other words, we wrap the code for which we want to check errors; the catch block handles exceptions in this block.

Here’s the try…catch code to handle errors:

var fs = require('fs')

try {
const data = fs.readFileSync('/Users/Kedar/node.txt')
} catch (err) {
  console.log(err)
}

console.log("an important piece of code that should be run at the end")

We receive the output as shown below:

$node main.js
{ Error: ENOENT: no such file or directory, open '/Users/Kedar/node.txt'
    at Error (native)
    at Object.fs.openSync (fs.js:641:18)
    at Object.fs.readFileSync (fs.js:509:33)
    at Object.<anonymous> (/home/cg/root/7717036/main.js:4:17)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)
    at Function.Module._load (module.js:438:3)
    at Module.runMain (module.js:604:10)
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/Users/Kedar/node.txt' }
an important piece of code that should be run at the end

The error is processed and displayed. In the end, the rest of the code executes as planned.

Callbacks

A callback function (commonly used for asynchronous code) is an argument to the function in which we implement error handling.

The purpose of a callback function is to check the errors before the result of the primary function is used. The callback is usually the final argument to the primary function, and it executes when an error or outcome of the operation emerges.

Here’s the syntax for a callback function:

function (err, result) {}

The first argument is for an error, and the second is for the result. In case of an error, the first attribute will contain the error, and the second attribute will be undefined and vice versa. Let’s check out an example where we try to read a file by applying this technique:

const fs = require('fs');

fs.readFile('/home/Kedar/node.txt', (err, result) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(result);
});

The result looks like this:

$node main.js
{ Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'
    at Error (native)
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt' }

We received the error because the file is not available. We can also implement the callbacks with the user-defined functions. The example below illustrates a user-defined function to double the given number using the callbacks:

const udf_double = (num, callback) => {
  if (typeof callback !== 'function') {
    throw new TypeError(`Expected the function. Got: ${typeof callback}`);
  }

  // simulate the async operation
  setTimeout(() => {
    if (typeof num !== 'number') {
      callback(new TypeError(`Expected number, got: ${typeof num}`));
      return;
    }

    const result = num * 2;
    // callback invoked after the operation completes.
    callback(null, result);
  }, 100);
}

// function call
udf_double('2', (err, result) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(result);
});

The program above will throw an error since we pass the string instead of an integer. The result is as follows:

$node main.js
TypeError: Expected number, got: string
    at Timeout.setTimeout (/home/cg/root/7717036/main.js:9:16)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)

Promises

Promise in Node.js is a contemporary way to handle errors, and it is usually preferred compared to callbacks. Since promises are alternatives to callbacks, let’s convert the example discussed above (udf_double) to utilize promises:

const udf_double = num => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof num !== 'number') {
        reject(new TypeError(`Expected number, got: ${typeof num}`));
      }

      const result = num * 2;
      resolve(result);
    }, 100);
  });
}

In the function, we will return a promise, which is a wrapper to our primary logic. We pass two arguments while defining the Promise object:

  1. resolve — used to resolve promises and provide results
  2. reject — used to report/throw errors

Now, let’s execute the function by passing the input:

udf_double('8')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

We get an error, as shown below:

$node main.js
TypeError: Expected number, got: string
    at Timeout.setTimeout (/home/cg/root/7717036/main.js:5:16)
    at ontimeout (timers.js:386:14)
    at tryOnTimeout (timers.js:250:5)
    at Timer.listOnTimeout (timers.js:214:5)

Well, this looks much simpler than callbacks. We can also use a utility such as util.promisify() to convert callback-based code into a Promise. Let’s transform the fs.readFile example from the callback section to use promisify:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

readFile('/home/Kedar/node.txt')
  .then((result) => console.log(result))
  .catch((err) => console.error(err));

Here we are promisifying the readFile function. We get the result as below:

[Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt'
}

Async/await

Async/await is just syntactic sugar that is meant to augment promises. It provides a synchronous structure to asynchronous code. For simple queries, Promises can be easy to use.



Still, if you run into scenarios with complex queries, it’s easier to understand the code that looks as though it’s synchronous.

Note that the return value of an async function is a Promise. The await waits for the promise to be resolved or rejected. Let’s implement the readFile example using async/await:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

const read = async () => {
  try {
    const result = await readFile('/home/Kedar/node.txt');
    console.log(result);
  } catch (err) {
    console.error(err);
  }
};

read()

We are creating the async read function in which we are reading the file using await. The output is as below:

[Error: ENOENT: no such file or directory, open '/home/Kedar/node.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/home/Kedar/node.txt'
}

Event emitters

We can use the EventEmitter class from the events module to report errors in complex scenarios — lengthy async operations that can produce numerous failures. We can continuously emit the errors caused and listen to them using an emitter.

Let’s check out the example where we try to mimic a receiving data scenario and check if it is correct. We need to check if the first six indexes are integers, excluding the zeroth index. If any index among the six is not an integer, we emit an error, making further decisions based on this error:

const { EventEmitter } = require('events'); //importing module

const getLetter = (index) =>{
    let cypher = "*12345K%^*^&*" //will be a fetch function in a real scenario which will fetch a new cypher every time
    let cipher_split = cipher.split('')
    return cipher_split[index]
}

const emitterFn = () => {
  const emitter = new EventEmitter(); //initializing new emitter
  let counter = 0;
  const interval = setInterval(() => {
    counter++;
    
    if (counter === 7) {
      clearInterval(interval);
      emitter.emit('end');
    }
    
    let letter = getLetter(counter)
    
    if (isNaN(letter)) { //Check if the received value is a number
      (counter<7) && emitter.emit(
        'error',
        new Error(`The index ${counter} needs to be a digit`)
      );
      return;
    }
    (counter<7) && emitter.emit('success', counter);

  }, 1000);

  return emitter;
}

const listner = emitterFn();

listner.on('end', () => {
  console.info('All six indexes have been checked');
});

listner.on('success', (counter) => {
  console.log(`${counter} index is an integer`);
});

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

Firstly, we import the events module to use EventEmitter. Then we define the getLetter() function to fetch the new cipher and send value on a particular index whenever requested by emitterFn(). The emitterFn() will initiate the EventEmitter object. We fetch the value on all six indexes one by one and emit an error if it is not an integer.

A variable stores the value received from emitterFn(), and we listen to them using listener.on(). After checking all the indexes, the program will end. The output looks as shown below:

1 index is an integer
2 index is an integer
3 index is an integer
4 index is an integer
5 index is an integer
The index 6 needs to be a digit
All six indexes have been checked

Handling errors

Now that you know the techniques to report errors, let’s handle them.

Retry the operation

Sometimes, errors can be caused by the external system for valid requests. For example, while fetching some coordinates using an API, you get the error 503, service not available, which is caused due to overload or a network failure.

At this point, the service might be back in a few seconds, and reporting an error might not be the ideal thing to do, so you retry the operation. Also, this may not be a good idea if you are deep down the stack because all layers keep retrying the process, and the wait time extends heavily. In such cases, it’s better to abort and let clients retry from their side.

Report the failure to the client

While receiving the wrong input from the client, retrying doesn’t make sense because we might get the same result upon reprocessing the incorrect information. In such cases, the most straightforward way is to finish the rest of the processing and report it to the client.


More great articles from LogRocket:


Report failures directly top of the stack

Sometimes it’s appropriate to report the errors directly because you might know the cause. For example, the ENOENT error discussed in the try…catch blocks section is generated when you are trying to open a file that is not present, and you can use any of the methods discussed above to report it. In this way, you can report creating the file to solve the error.

Crash immediately

In case of unrecoverable errors, crashing the program is the best option. For example, an error is caused due to accessing a file without permission; there is nothing you can do instead of crashing the system and letting sysadmin provide the access. Crashing is also the most practical way to deal with programmer errors to recover the system.

Conclusion

To conclude, appropriate error handling is mandatory if you strive to write good code and deliver reliable software. In this post, we learned about errors and the importance of handling them in Node.js. We discussed preliminary ways to report the errors: try…catch blocks, callbacks and promises, async/await syntax, and event emitters. We also learned to handle these errors once they are reported.

Lastly, I hope it was helpful to you, and now you can handle errors in your Node.js application.

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Kedar Kodgire Kedar is a tech blogger, UI/UX designer and developer, cloud engineer, and machine-learning enthusiast. He's passionate about exploring new technologies and sharing them with everyone.

Leave a Reply