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:
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
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 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:
x
object is not iterableEvery 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:
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.
To handle the errors effectively, we need to understand the error delivery techniques.
There are four fundamental strategies to report errors in Node.js:
try…catch
blocksLet’s understand using them one by one.
try…catch
blocksIn 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.
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)
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:
resolve
— used to resolve promises and provide resultsreject
— used to report/throw errorsNow, 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 is just syntactic sugar that is meant to augment promises. It provides a synchronous structure to asynchronous code. For simple queries, Promise
s
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' }
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
Now that you know the techniques to report errors, let’s handle them.
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.
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.
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.
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.
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.
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.
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. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
One Reply to "Error handling in Node.js"
Thanks a ton!!