Errors are a constant presence in application development. There's a saying that as long as there is code, there will always be "bugs". There are obvious errors that we can anticipate, and then there are those unknown errors that can catch us by surprise.
Errors can cause inconvenience and sometimes even have serious consequences. Therefore, error handling is always an important aspect of programming. In this article, I will present some "elegant" error handling techniques in the Node.js environment.
Avoid using callbacks that can result in callback hell, making your code deeply nested and hard to read and maintain.
Here's an example of using callbacks to handle errors:
getData(someParameter, function(err_a, a) {
if(err_a!== null) {
getMoreDataA(a, function(err_b, b) {
if(err_b !== null) {
getMoreDataB(b, function(err_c, c) {
getMoreDataC(c, function(err_d, d) {
if(err_d !== null ) {
// do something
}
})
});
}
});
}
});
Instead, use promises to handle errors in a more "elegant" way:
return getData(someParameter)
.then(getMoreDataA)
.then(getMoreDataB)
...
.catch(err => handle(err));
However, promises can make debugging difficult. Use the "graceful" syntax of async/await combined with try/catch:
try {
const a = await getData(someParameter);
const b = await getData(someParameter);
} catch(err) {
handle(err);
}
Error
object instead of any other object when reporting an errorJavaScript allows us to "throw" any object to report an error, whether it's a number, a string, or an object. However, it's best to throw an Error
object to ensure consistency in your code and with libraries. Furthermore, an Error
object retains important information such as the StackTrace, which indicates where the error occurred.
// don't do this
if (!condition) {
throw ('invalid condition');
}
// do this instead
if (!condition) {
throw new Error('invalid condition');
}
// or you can even do better by creating a custom error object
// that carries more useful information
function MyError(name, description, ...args) {
Error.call(this);
Error.captureStackTrace(this);
this.name = name;
this.description = description;
...
};
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
if (!condition) {
throw new MyError('CONDITION_NOT_VALID', 'invalid condition');
}
Create one or more functions that are ready to receive an error object and then analyze or distribute the error to different places. Avoid handling errors separately, as this can make your code harder to control.
Imagine what you would do when an error occurs. Do you log it to the console, send it to monitoring or logging services, write it to a file, check conditions and classify the error... there are many things to do with an error once it is thrown. Therefore, gather them in one or more specialized error handling functions.
unhandledRejection
errorsThis is a very annoying error in Node.js that can cause your application to hang and no longer process any requests. This error occurs when you mishandle a promise, specifically when there is no error handling function for reject
in the promise.
UserModel.findByPk(1).then((user) => {
if(!user)
throw new Error('user not found');
});
To catch this error, we need to use process.on
.
process.on('unhandledRejection', (reason, p) => {
throw reason;
});
process.on('uncaughtException', (error) => {
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error))
process.exit(1);
});
process.on
is usually placed in the index.js
file, which is the first file that your application starts with, so that it can "listen" to any signals coming from mishandling promises. This allows you to handle the error properly and prevent the server from hanging.
We provide POST, PUT... endpoints to allow clients to send data to our application, but the data cannot always be trusted. Therefore, the safest approach is to always validate the received data to prevent unintentional or malicious submission of incorrect data, which can cause errors or even system breaches.
There are many excellent libraries that can help with this, such as Joi or ajv.
const schema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required(),
password: Joi.string()
.pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
});
let body = { username: '2coffee', password: '2coffee' };
schema.validate(body); // -> { value: { username: '2coffee', password: '2coffee' } }
let body = {};
schema.validate(body); // -> { value: {}, error: '"username" is required' }
Logging is a way to track and monitor errors that have occurred in the past, allowing you to retrieve them at any time. I have a separate article on logging in Node.js applications, which you can read at Logging in Node.js Applications at 3 Different Levels.
Unit tests are one of the methods that help you catch errors early in the development process. While writing unit tests can be time-consuming, it is worth investing your time in them. Once your unit tests are working well, they will save you a lot of time and cost in the long run.
In cases where you encounter specific errors for which there is no immediate solution or recoverable state, the temporary solution is to crash the application and rely on DevOps tools to restart it. This may not be the best approach, but restarting your application will restore it to the state before the error occurred.
Application Performance Management (APM) is used to monitor and manage the performance and availability of an application. APM tries to detect and diagnose performance issues and promptly notify you when there's a problem.
There are many APM products and services in the market, ranging from open-source to paid solutions. The features vary from simple to complex, from basic monitoring to deep integration for monitoring complex systems.
Start with a simple APM service like uptimerobot.com. It monitors the uptime of your application by sending a request every 5 minutes and waiting for a successful or failed response.
New Relic is a more comprehensive tool that monitors various aspects such as detailed error stack traces, response times, performance bottlenecks, and detailed statistics.
These are some error handling techniques that I recommend for Node.js applications. There may be other techniques that I'm not aware of, so if you discover any missing techniques, please leave a comment below.
5 profound lessons
Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!
Subscribe to receive new article notifications
Comments (1)