Some error handling techniques in Node.js

Some error handling techniques in Node.js

The problem

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.

Techniques

Use async/await or promises to handle errors in asynchronous functions

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);
}

Throw an Error object instead of any other object when reporting an error

JavaScript 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');
}

Centralize error handling

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.

Beware of unhandledRejection errors

This 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.

Never trust input data, always validate

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' }

Use a professional logging system

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.

Write unit tests

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.

"Exit" the application when necessary

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.

Use APM (Application Performance Management)

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.

banner

New Relic is a more comprehensive tool that monitors various aspects such as detailed error stack traces, response times, performance bottlenecks, and detailed statistics.

banner

Conclusion

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.

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.
Author

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (1)

Avatar
Jess Vanes2 years ago
Hướng dẫn dùng mấy tool apm đi ạ
Reply
Avatar
Xuân Hoài Tống2 years ago
@gif [ISOckXUybVfQ4] Chắc là sẽ hơi lâu ạ