‘Error’, No one from us likes this word but this is fact No one is perfect in this world including machines. None of our days pass without having errors faced in our professional life. Whenever we are facing any bugs and errors while working rather than worrying, let us all fix our mind like we are going to learn something new. To resolve errors first we have to manage error handling in Node Js
What is Error Handling in Node Js?
It’s a way to see bugs in our code. Following this logic, error handling is a way to find these bugs and solve them as quickly as humanly possible. Handling errors properly means developing a robust codebase and reducing development time by finding bugs and errors easily.
Why Error Handling is Important?
If we want to make bug fixing less painful. It helps us to write cleaner code. It centralizes all errors and let’s enable alerting and notifications so we know when and how our code breaks.
Which Type of Errors can Occur and which Errors We can Handle?
There are two main types of errors in node.js. Errors can be operational or programmer. See what they are and how they occur in our code.
Operational Errors | Programmer Errors | |
Type | Represent runtime problems | Represent unexpected issues |
Source | System configurations, the network or the remote services. | Bugs or faults which represent unexpected issues in a poorly written code |
How to Fix | By resolving network or system or configurations | Review code and error logs and handle those properly with logic |
Examples |
|
|
How to handle errors in NodeJS?
Before we start with error handling, let’s understand the error object first.
Error Object
The error object is a built-in object in the Node.js runtime. It gives us a set of info about an error when it happens.
A basic error looks like this:
const error = new Error("Error message"); console.log(error); console.log(error.stack);
It has an error.stack field that gives us a stack trace showing where the error came from. It also lists all functions that were called before the error occurred. The error.stack field is optimal to use while debugging as it prints the error.message as well.
{ stack: [Getter/Setter], arguments: undefined, type: undefined, message: 'Error message' } Error: The error message at Object.<anonymous> (/home/src/blog.js:1:75) at Module._compile (module.js:407:26) at Object..js (module.js:413:10) at Module.load (module.js:339:31) at Function._load (module.js:298:12) at Array.0 (module.js:426:10) at EventEmitter._tickCallback (node.js:126:26)
We can also add more properties if we want to know some more information about the Error object.
const error = new Error("Error message"); error.status_code = 404; console.log(error);
Best Practices to Handle Errors in NodeJS
Before entering into how to handle errors in node.js in a best way we should know how Node.js architecture, frameworks and libraries work with all the developer practices. Do not repeat mistakes rather handle them with utmost care to resolve them faster. Here are some of the best ways to handle all our errors in our application.
Handling asynchronous errors
Using Callbacks
Callbacks were the most basic and oldest way of delivering the asynchronous errors. We pass a callback function as a parameter to the calling function, which we later invoke when the asynchronous function completes executing.
Usual Callback Pattern
ccallback(err, result);
function myAsyncFunction(callback) { setTimeout(() => { callback(new Error('oops')) }, 1000); } myAsyncFunction((err) => { if (err) { // handle error } else { // happy path } })
Using Promises
Promises are the new and improved way of writing asynchronous code which can be used to replace callback methods. Use .catch() while handling errors using promises.
doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => console.error(err))
function myAsyncFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('oops')) }, 1000); }) } myAsyncFunction() .then(() => { // happy path }) .catch((err) => { // handle error });
Using async-wait
To catch errors using async-wait method we can do it this way:
function myAsyncFunction() { return new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('oops')) }, 1000); }); } (async() => { try { await myAsyncFunction(); // happy path } catch (err) { // handle error } })();
Using EventEmitter
In some cases, we can’t rely on promise rejection or callbacks. Examples like reading files from a stream or fetching rows from a database and reading them as they arrive. We can’t rely on one error because we need to listen for error events on the EventEmitter object.
In this case, instead of returning a Promise, our function would return an EventEmitter and emit row events for each result, an end event when all results have been reported, and an error event if any error is encountered. In the below example the socket value is an EventEmitter object.
net.createServer(socket => { ... socket .on('data', data => { ... }) .on('end', result => { … }) .on('error', console.error) // handle multiple errors }
Use a Middleware
We can configure a centralized error handling in Node Js method when we have a set of custom errors. To catch all the errors, we can use middleware and from there we can decide whether to log all the errors or we need to get notified whenever an error occurs.
To forward the errors to the error handler middleware use the next() function.
app.post('/user', async (req, res, next) => { try { const newUser = User.create(req.body) } catch (error) { next(error) } })
Catch All Uncaught Exceptions
When unexpected errors occur, we want to handle it immediately by sending a notification and restarting the app to avoid unexpected behavior.
const { logError, isOperationalError } = require('./errorHandler') ... process.on('uncaughtException', error => { logError(error) if (!isOperationalError(error)) { process.exit(1) } }) ...
Catch All Unhandled Promise Rejections
Promise rejections in Node.js only cause warnings. We want them to throw errors, so we can handle them properly. It’s good practice to use fallback and subscribe to:
process.on('unhandledRejection', callback)
This let us throw an error properly. Here’s what the error handling in Node Js flow should look like.
... const user = User.getUserById(req.params.id) .then(user => user) // missing a .catch() block ... // if the Promise is rejected this will catch it process.on('unhandledRejection', error => { throw error }) process.on('uncaughtException', error => { logError(error) if (!isOperationalError(error)) { process.exit(1) } })
Use the Appropriate Log Levels for Errors and Error Alerting
We can also log all the error messages at different log levels which can then be sent to different destinations such as stdout, syslog, files etc. We should also opt the perfect log levels for our message based on the priority of log messages. Here are some of the basic log levels which could be used often.
log.info – If informative messages occur frequently, it could become a noise. These messages are used for reporting significant successful actions.
log.error – All critical error messages which require instant attention and could possibly cause any dire consequences.
log.warn – This warning message occurs when something unusual happens which is not critical but it could be useful if we review it and resolve the error.
log.debug – These messages are not very crucial but could be useful while debugging.
Use a Centralized Location for Logs and Error Alerting
The advantage of it is to use structured logging to print errors in a formatted way and send them for safekeeping to a central location, like any log management tool. It’ll help us to persist the logs over time, so we can go back and troubleshoot issues whenever things break.
To do this, we should use loggers like winston or any other logging packages. First, create a setup for winston. Create a loggers directory and a logger.js file. Paste this into the file.
// logger.js const winston = require('winston') const options = { console: { level: 'debug', handleExceptions: true, json: false, colorize: true } } const logger = winston.createLogger({ levels: winston.config.npm.levels, transports: [ new winston.transports.Console(options.console), ], exitOnError: false }) module.exports = logger
The good thing with this is that we get JSON formatted logs we can analyze to get more useful information about our app. We’ll also get all logs forwarded to Logger. This will alert us whenever errors occur. That’s pretty awesome!
In errorHandler.js we can now replace all console.error() statements with logger.error() to persist the logs in Logger.
// errorHandler.js const logger = require('../loggers/logger') const BaseError = require('./baseError') function logError (err) { logger.error(err) } function logErrorMiddleware (err, req, res, next) { logError(err) next(err) } function returnError (err, req, res, next) { res.status(err.statusCode || 500).send(err.message) } function isOperationalError(error) { if (error instanceof BaseError) { return error.isOperational } return false } module.exports = { logError, logErrorMiddleware, returnError, isOperationalError }
That’s it, we now know how to handle errors properly!
Mr. Vaibhav Shah