As unpleasant as they are, errors are crucial to software development.
When developing an application, we usually don't have full control over the parties interacting with a program and its hosts (including operating system versions, processors, and network speed).
It's important you have an error reporting system to diagnose errors and make errors human-readable.
In this post, we'll first look at the two common types of errors. We'll then explore how to handle errors in Node.js and track them using AppSignal.
Let's get started!
Common Types of Errors
We will discuss two types of errors in this article: programming and operational errors.
Programming Errors
These errors generally occur in development (when writing our code). Examples of such errors are syntax errors, logic errors, poorly written asynchronous code, and mismatched types (a common error for JavaScript).
Most syntax and type errors can be minimized by using Typescript, linting, and a code editor that provides autocomplete and syntax checking, like Visual Studio Code IntelliSense.
Operational Errors
These are the kind of errors that should be handled in our code. They represent runtime problems that occur in operational programs (programs not affected by programming errors). Examples of such problems include invalid inputs, database connection failure, unavailability of resources like computing power, and more.
For instance, let's say you write a program using the Node.js File System module (fs
) to do some operations on a jpeg file. It’s a good practice to enable that program to handle most common edge cases (like uploading non-image files). However, a lot of problems may occur in your program depending on where or how fs
is used, especially where you have no control over them (in the processors or Node version on the host, for example).
Errors in Node.js
Errors in Node.js are handled as exceptions. Node.js provides multiple mechanisms to handle errors.
In a typical synchronous API, you will use the throw
mechanism to raise an exception and handle that exception using a try...catch
block. If an exception goes unhandled, the instance of Node.js that is running will exit immediately.
For an asynchronous-based API, there are many options to handle callbacks — for instance, the Express.js default error-first callback argument.
Custom Error: Extending the Error Object in Node.js
All errors generated by Node.js will either be instances of — or inherit from — the Error
class. You can extend the Error
class to create categories of distinct errors in your application and give them extra, more helpful properties than the generic error.message
provided by the error class. For instance:
class CustomError extends Error {
constructor(message, code, extraHelpFulValues) {
super(message);
this.code = code;
this.extraHelpFulValues = extraHelpFulValues;
Error.captureStackTrace(this, this.constructor);
}
helpFulMethods() {
// anything goes like log error in a file,
}
}
class AnotherCustomError extends CustomError {
constructor(message) {
super(message, 22, () => {});
// ...
}
}
Handling Errors in Node.js
Once an exception is unhandled, Node.js shuts down your program immediately. But with error handling, you have the option to decide what happens and handle errors gracefully.
Try Catch in Node
try...catch
is a synchronous construct that can handle exceptions raised within the try
block.
try {
doSomethingSynchronous();
} catch (err) {
console.error(err.message);
}
So if we have an asynchronous operation within the try
block, an exception from the operation will not be handled by the catch
block. For instance:
function doSomeThingAsync() {
return new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("some error")), 5000)
);
}
try {
doSomeThingAsync();
} catch (err) {
console.log("ERROR: ", err);
}
Even though the doSomethingAsync
function is asynchronous, try...catch
is oblivious to the exception raised in the asynchronous function.
Some versions of Node.js will complain about having unhandled exceptions. The -unhandled-rejections=mode
is a very useful flag to determine what happens in your program when an unhandled exception occurs.
Let's now look at more ways to handle exceptions raised in asynchronous operations.
Error-First Callback Error Handling
This is a technique for handling asynchronous errors used in most Callback APIs. With this method, if the callback operation completes and an exception is raised, the callback's first argument is an Error
object. Otherwise, the first argument is null
, indicating no error.
You can find this pattern in some Node modules:
const fs = require("fs");
fs.open("somefile.txt", "r+", (err, fd) => {
// if error, handle error
if (err) {
return console.error(err.message);
}
// operation successful, you can make use of fd
console.log(fd);
});
So you can also create functions and use Node’s error-first callback style, for instance:
function doSomethingAsync(callback) {
// doing something that will take time
const err = new CustomerError("error description");
callback(err /* other arguments */);
}
Promise Error Handling
The Promise
object represents an asynchronous operation's eventual completion (or failure) and its resulting value.
We mentioned above how the basic try...catch
construct is a synchronous construct. We proved how the basic catch
block does not handle exceptions from these asynchronous operations.
Now we'll discuss two major ways to handle errors from asynchronous operations:
- Using
async/await
with thetry...catch
construct - Using the
.catch()
method on promises
Async/Await Error Handling with Try Catch
The regular try...catch
is synchronous. However, we can make try...catch
handle exceptions from asynchronous operations by wrapping try...catch
in an async
function. For example:
function doSomeThingAsync() {
return new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("some error")), 5000)
);
}
async function run() {
try {
await doSomeThingAsync();
} catch (err) {
console.log("ERROR: ", err);
}
}
run();
When running this code, the catch
block will handle the exception raised in the try
block.
Using the .catch()
Method
You can chain a .catch
to handle any exception raised in Promise
. For instance:
function doSomeThingAsync() {
return new Promise((resolve, reject) =>
setTimeout(() => reject(new Error("some error")), 5000)
);
}
doSomeThingAsync()
.then((resolvedValue) => {
// resolved the do something
})
.catch((err) => {
// an exception is raised
console.error("ERROR: ", err.message);
});
Event Emitters
EventEmitter is another style of error handling used commonly in an event-based API. This is exceptionally useful in continuous or long-running asynchronous operations where a series of errors can happen. Here's an example:
const { EventEmitter } = require("events");
class DivisibleByThreeError extends Error {
constructor(count) {
super(`${count} is divisible by 3`);
this.count = count;
}
}
function doSomeThingAsync() {
const emitter = new EventEmitter();
// mock asynchronous operation
let count = 0;
const numberingInterval = setInterval(() => {
count++;
if (count % 3 === 0) {
emitter.emit("error", new DivisibleByThreeError(count));
return;
}
emitter.emit("success", count);
if (count === 10) {
clearInterval(numberingInterval);
emitter.emit("end");
}
}, 500);
return emitter;
}
const numberingEvent = doSomeThingAsync();
numberingEvent.on("success", (count) => {
console.log("SUCCESS: ", count);
});
numberingEvent.on("error", (err) => {
if (err instanceof DivisibleByThreeError) {
console.error("ERROR: ", err.message);
}
// other error instances
});
In the sample above, we create a custom error called DivisibleByThreeError
and also a function that creates an event emitter. The function emits both success
and error
events at different intervals, running a counter from 1 to 10 and emitting an error when the counter is divisible by 3.
We can listen to error
events, determine the type of error, and then act accordingly or end the program.
Using AppSignal to Track Node.js Errors
Your Node.js project will usually run on various hosts (such as different operating systems and processors) with different network setups and network speed structures, for example.
An Application Performance Management (APM) tool like AppSignal monitors your application, so you can easily track error alerts and keep your errors under control.
From the issue list, you can dig into errors and see a description of each incident, as well as when it happened in your app. Here's how it looks in AppSignal:
Get started with AppSignal for Node.js.
Wrap Up
In this article, we explored errors in Node.js and touched on the many methods we can use to handle errors, including:
- Using the
try...catch
block - An error-first callback style
- Using
try...catch
with async/await -
Promise
with.catch
- Event emitters
We also discussed using AppSignal to track errors.
Happy coding!
P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.
P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.
Top comments (0)