A Guide To Node.js Error Handling
Thursday, December 28, 2023It’s an inevitable fact that errors will arise during software application development. Error handling in Node.js is not straightforward, so Node.js development companies must be prepared to effectively address both operational and syntax errors when creating production-ready software solutions.
Node.js developers rigorously test their code against complex edge cases to ensure smooth functioning during code deployment in production. To achieve this, developers must have a thorough understanding of various Node.js error handling techniques, such as the try…catch block, Event Emitters, promises, and more.
In this article , we will understand different types of programming errors that might occur in Nodejs app development process and what are the techniques to handle the errors.
1. Types of Errors in NodeJS
Two different categories, namely operational errors and programmer errors, divide errors.
1.1 Operational Errors
Runtime problems generally expect operation errors to occur during the application runtime. Operational errors do not necessarily mean that they are software bugs in the application but they will hinder the app processing.
Here are some of the common operational errors.
- Invalid user input
- Internal server error
- Socket hang-up
- Request timeout
- Failed to resolve the hostname
- System in out of memory
- The server returned a 500 response
1.2 Programmer Errors
Programmer errors, also referred to as software bugs, are errors in code that result in undesired behavior. These errors occur due to syntax errors, print errors, or logic errors.
For instance, attempting to read a property of an undefined object in Node.js code represents the classic form of a programmer error. Besides this, the majority of applications experience some programmer errors either before or after deployment.
- Missing a rejected promise
- Not resolving a promise
- Calling an asynchronous function without a callback function
- Passing incorrect parameters
- Passing an object instead of a line
- Passing a string instead of an object
The above-defined error occurs while working with Node.js and as stated there are two types of errors, operational errors occur at runtime, and programmer errors are the ones that occur in the code. The reason why they are bifurcated into two different categories is –
- If you don’t find a user in the application, you won’t stop the entire app as other users are still present, this is a situation of an operational error.
- On the other hand, if you neglect to catch a rejected promise in the codebase, causing a bug in the application, making the application run will be a mistake. In this case, a programmer error necessitates restarting the application.
Now that we understand both types of errors in Node.js, let’s explore techniques that assist developers with implementing proper error-handling approaches, along with ideas applicable to any type of centralized error-handling component.
2. Error Handling Techniques
The list of techniques used for handling errors in Node.js is here.
2.1 Try…catch Blocks
The very first technique in our list is the try…catch method. In this method, the try block will surround the code where the error might have occurred. This implies that the developer will wrap the code in the area where they want to check for errors, and then they will use the catch block to handle exceptions in this block.
Below is an example of the try…catch blocks and how they handle errors in the Node.js code.
var fs = require('fs')
try {
const data = fs.readFileSync('/Users/Abc/node.txt')
} catch (err) {
console.log(err)
}
console.log("an important piece of code that should be run at the end")
Below given is the example of the output that the developer can find after running the try…catch blocks.
The code processed and displayed the error in the code in the above output, and the rest of the code executed as intended.
2.2 Node.js Error Handling using a Callback Function
Another type of error handling in Node.js is using a callback function. In JavaScript functions, developers generally use a callback function as an argument for handling errors in asynchronous code implementation. The primary purpose of this error-handling approach is to verify whether errors occur before utilizing the result of the main function. The rationale behind this verification is that the callback serves as the final argument for the primary function, ensuring its execution only when the outcome or an error from the operation has surfaced.
Below is the syntax of a callback function.
function (err, result) {}
In the above function, the first argument is for an error and the second argument is for the result in the code. This means that if any type of software error occurs, the first attribute will carry the error and the second will be undefined. Developers can use the example below to read the file by applying a callback function technique for Node.js error handling.
const fs = require('fs');
fs.readFile('/home/Abc/node.txt', (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result);
});
The result of the above code is given below.
As we can see in the above output, the error we received is because the file we were looking for isn’t available.
Besides this, callback functions can also be implemented with the help of user-defined functions. In the below-given example, we can see how a user-defined function doubles the given number with the help of callbacks.
// Define a function 'udf_double' that takes a number 'num' and a 'callback' function as parameters.
const udf_double = (num, callback) => {
// Check if the 'callback' is a function, and throw an error if it's not.
if (typeof callback !== 'function') {
throw new TypeError(`Something went wrong. Expected the function. Got: ${typeof callback}`);
}
// simulate the async operation
setTimeout(() => {
// Check if 'num' is not a number, and call the 'callback' with an error if it's not.
if (typeof num !== 'number') {
callback(new TypeError(`Oops! Something went wrong. Expected a number, but received a value of type ${typeof num}.`));
return;
}
const result = num / 2;
// callback invoked after the operation completes.
callback(null, result);
}, 100);
}
// function call
udf_double('4', (err, result) => {
if (err) {
console.error(err)
return
}
console.log(result);
});
The above code will throw an error as instead of an integer, the string has been passed.
The result of the above code will be as follows.
2.3 Node.js Error Handling in Promises
Now, let’s explore another Node.js error-handling technique that utilizes Promises. In Node.js, Promises represent a modern approach to error handling and are often compared to callbacks. The comparison arises because promises are typically viewed as alternatives to callbacks in Node.js.
Example provided below, we will demonstrate how to convert the given code (udf_double) to use promises.
const udf_double = num => {
// Create and return a new Promise to handle asynchronous operations.
return new Promise((resolve, reject) => {
setTimeout(() => {
// Check if 'num' is not a number, and reject the Promise with an error if it's not.
if (typeof num !== 'number') {
reject(new TypeError(Oops! Something went wrong. Expected a number, but received a value of type ${typeof num}));
}
const result = num * 2;
resolve(result);
}, 100);
});
}
In the above code, we observe that a function will return a promise, considering it a wrapper for the primary logic. In this scenario, the developer can specify two arguments when defining the Promise object: the first argument is “resolve,” used for resolving promises and providing results, and the second argument is “reject,” used for reporting or throwing errors.
Following this, one can execute the function by passing the input, as demonstrated in the code below.
udf_double('8')
.then((result) => console.log(result))
.catch((err) => console.error(err));
When the function is executed after passing an input, the below-shown error can be fetched.
As you can see in the above output, the result here looks much simpler than callbacks. In such situations, the developers can also use a utility like a util.promisify() in order to convert callback-based code into a Promise.
In the below code, we will transform the fs.readFile example from the callback section in order to use Promisify.
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('/home/Abc/node.txt')
.then((result) => console.log(result))
.catch((err) => console.error(err));
The above code is where we have promised the readFile function.
Here is the result of the above code.
2.4 Extending the Error Object
Another widely used Node.js error handling approach is extending the error object. Here the developers can use a generic instance or the built-in error classes of the Error object which is normally not precise to communicate with various other types of errors that might occur in the code. This is why it becomes really important to create custom error classes. This can reflect the types of errors in a better way. For instance, there is a ValidationError class in the code for errors that have occurred while the codebase was validating user input, there is TimeoutError for operations, and DatabaseError class for database operations.
This shows that any type of custom error class that extends the Error object will have the capability to retain the basic properties of that particular error such as error name, error message, and more. Besides this, developers can enhance a ValidationError by adding meaningful properties like input portions that were the reasons behind the errors.
Below is the code that is the perfect example of how the developer can extend the built-in Error object in Node.js.
class ErrorApplication extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends ErrorApplication {
constructor(message, cause) {
super(message);
this.cause = cause
}
}
In the above code, the ErrorApplication class is known as a generic error for the application. But on the other hand, the ValidationError class is for any type of error that might occur while validating user input. This class inherits from the ErrorApplication class and then it augments itself with a cause property in order to define the input that is the reason behind the error getting triggered.
Custom errors can be used in the code just like normal errors. Here is an example of it.
function validateInput(input) {
if (typeof input !== 'number'
) {
throw new ValidationError('Expected number, but received a value of type ${typeof input}.
);
}
return input;
}
try {
validateInput("not a number"
);
} catch (err) {
if (err instanceof ValidationError) {
console.error(`Validation error: ${err.message}, caused by: ${err.cause}`);
return;
}
console.error(`Other error: ${err.message}`);
}
Here is the output of the above code.
2.5 Event Emitters
Event Emitters is the last type of error handling in our list. The developers can use the EventEmitter class to report errors in a complex scenario and this can be done from the events module. Here, complex scenarios mean lengthy async operations in the Node.js code that can be the reason behind various failures. Such failures can be emitted by emitting the errors can listening to them by using an emitter.
In the below-given example, we will see how a developer can receive data and check whether it is correct or not using the Event Emitters technique. We will also find out if the first six indexes are integers, which will not include the zeroth index, and if any index among the first six is not an integer, it will be an emit error.
const { EventEmitter } = require('events'); //importing module
// Function to get a letter from the cypher at a specified index
const getLetter = (index) =>{
let cypher = "A1B2C3D4E5F6
" //will be a fetch function in a real scenario which will fetch a new cypher every time
let cypher_split = cypher
.split('')
return cypher_split
[index]
}
const emitterFn = () => {
const emitter = new EventEmitter(); //initializing new emitter
let counter = 0;
const interval = setInterval(() => {
counter++;
if (counter === 8) {
clearInterval(interval);
emitter.emit('end');
}
let letter = getLetter(counter)
if (isNaN(letter)) { //Check if the received value is a number
(counter {
console.info('All seven indexes have been checked');
});
listner.on('success', (counter) => {
console.log(`${counter} index is an integer`);
});
listner.on('error', (err) => {
console.error(err.message);
});
In the above code, we observed that after importing the events module to utilize EventEmitter, we defined the getLetter() function. This function can fetch the new cypher and then send a value retrieved from a specific index whenever emitterFn() requests it. The emitterFn() used in the above code initiates the EventEmitter object, enabling the retrieval of values from all seven indexes and making it possible to emit an error.
Here, the variable can store the value received from emitterFn() and listener.on() makes it possible for us to listen to them. After all the indexes are checked, the program will end by giving the following output.
3. Conclusion
As seen in this blog, the proper understanding of error handling is mandatory for any Node.js developer who wants to write good code and deliver bug-free & reliable software. There are some preliminary methods that can be used to report the errors in Node.js like try…catch blocks, callbacks, promises, error objects, and event emitters as discussed in this article. These error-handling approaches can enable the developers to offer production-ready applications.
4. Frequently Asked Questions
4.1 What is Errorhandler in node JS?
Node.js will terminate your application instantly if an unhandled error occurs. However, you get to choose what goes down and how issues are handled thanks to the error handler.
4.2 How many types of error handling are there in node JS?
There are two types of error handling in NodeJS:
- Operational Errors
- Programmers Errors
4.3 What is error-handling middleware in node JS?
The error-handling middleware is specified in a similar manner to other middleware functions, with the exception that error-handling functions require four parameters. These parameters are err, req, res, and next.
The error-handling middleware has the potential to be strategically positioned either after routes or to incorporate conditions that can identify error types and then provide appropriate responses to clients.
Comments