In this article, we will go to create meaningful exceptions names for Node & typescript & express application with global error handler middleware. Bit by bit, we will learn how to create custom errors and express middleware to handle descriptive and meaningful name errors. You will be able to capture and handle errors more effectively and, above all, learn what are the pros and cons of using meaningful exception names.
Let's make a fictitious situation:
In a software development company, a team of developers is working on a new e-commerce platform for a client. The developers are tasked with creating a number of features and functionalities for the website, including payment processing, user authentication, and stock availability. However, as the project moves on, the team starts to encounter numerous bugs and errors in their code, making it difficult to identify and address the root causes of these issues.
One of the biggest causes of these issues is because developers do not provide significant exceptions in their code. Instead, they are relying on generic error messages or not handling errors at all.
Realizing the importance of using meaningful exceptions, we should implement a more robust exception handling mechanism and introduce custom errors which allows us to identify and handle errors more effectively.
Given the above, let’s code the project.
Setting up the project
Firstly, we have to create our Node project:
npm init --yes # this will trigger automatically populated initialization with default values
Then, install the project's dependencies which are necessary to work with “Typescript” and “express”.
npm install express
# Dev dependencies
npm install --save-dev @types/express @types/node typescript ts-node ts-node-dev
Project structure
Add the following files and folders in your local project folder:
.
├── src
│ ├── errors
│ │ ├── base.error.ts
│ │ ├── insufficient-funds.error.ts
│ │ ├── types.ts
│ │ └── internal-server.error.ts
│ ├── middlewares
│ │ └── error.middleware.ts
│ └── server.ts
└── tsconfig.json
Typescript configurations
To setup typescript properly, add the following settings to the tsconfig.json file:
{
"compilerOptions": {
"target": "es2020",
"outDir": "./dist",
"rootDir": "src",
"moduleResolution": "node",
"module": "commonjs",
"declaration": true,
"inlineSourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"types": ["node"],
"typeRoots": ["node_modules/@types"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**"],
}
Types
We are going to define a set of types which helps us to add additional properties and methods for our custom errors. Add the following content into types.ts file:
export type ErrorPayload = {
message: string;
details: string | null;
};
export type ErrorOptions = {
httpCode: number;
payload: ErrorPayload;
};
export type ErrorResponse = {
httpCode: number;
error: {
name: string;
stack: Array<string> | string;
} & ErrorPayload;
};
Custom Errors
BaseError
As we already have Error types defined, we will create a base class for custom errors that extends the javascript built-in Error class. This base class would serve as a template for creating specific custom error classes that can be used throughout the application. Add the following content into base.error.ts file:
import { ErrorOptions, ErrorResponse } from "./types";
export abstract class BaseError extends Error {
constructor(private readonly options: ErrorOptions) {
super();
this.name = this.constructor.name;
}
toHttpResponse(): ErrorResponse {
const { httpCode, payload } = this.options;
return {
httpCode,
error: {
name: this.name,
...payload,
stack: (this.stack ?? "Stack not available").split("\n").map((item: string) => item.trim())
},
};
}
}
The class BaseError extends the built-in Error class. It takes an ErrorOptions object as a parameter in its constructor. The constructor sets the name property of the error to the constructor's name.
The toHttpResponse method is defined to convert the error object into an ErrorResponse object which includes the HTTP code and error details. It then returns an object with httpCode, error object containing the error name, payload, and a stack trace (if available).
Now, we can create specific custom error classes by extending the “BaseError” class.
Custom InsufficientFundsError
This custom error will represent a business rule for payment processing feature for those cases when the bank decline charge because the customer does not have available balance. Add the following content into insufficient-funds.error.ts:
import { BaseError } from "./base.error";
import { ErrorPayload } from "./types";
export class InsufficientFundsError extends BaseError {
constructor(payload: ErrorPayload) {
super({
httpCode: 400,
payload,
});
}
}
As the BaseError constructor expect an httpCode attribute we will use the value 400 which will represent the Bad Request http status code.
Custom InternalServerError
And we will create a default custom error which handle all of those exceptions that are not instance of BaseError. For this new custom error we will use the value 500 to represents the Internal Server Error http status code. Add the following content into internal-server.error.ts:
import { BaseError } from "./base.error";
export class InternalServerError extends BaseError {
constructor(error: Error) {
super({
httpCode: 500,
payload: {
message: error.name,
details: error.message,
},
});
}
}
Express Error Middleware
Then, add the following content into the error.middleware.ts file:
import { Express, NextFunction, Request, Response } from "express";
import { BaseError } from "../errors/base.error";
import { InternalServerError } from "../errors/internal-server.error";
export class ErrorMiddleware {
static handle(error: Error,
req: Request,
res: Response,
next: NextFunction): void {
let _error = <BaseError>error;
if (!(error instanceof BaseError)) {
_error = new InternalServerError(error);
}
const { httpCode, ...rest } = _error.toHttpResponse();
res.status(httpCode).json({ ...rest });
}
}
The ErrorMiddleware class contains a static method called handle. This method takes in four parameters according to the express error handler signature: error, request, response, and next. It first checks if the error is an instance of BaseError, if not, it creates a new instance of InternalServerError using the original error message.
It then extracts the httpCode and other properties from the error object using the toHttpResponse method of the BaseError class. Finally, it uses the extracted httpCode to sets the HTTP status code of the response and sends a JSON response with the remaining properties of the error object. The error object has the following properties:
name: This indicates the name.
message: This is a specific error message.
details: This provides more specific details about the error
stack: This is an array of stack trace information showing the sequence of function calls that led to the error. It includes information about the file path, line number, and function where the error occurred.
Express Server
Next, add the following content into the server.ts file:
import express, { Request, Response } from "express";
import { InsufficientFundsError } from "./errors/insufficient-funds.error";
import { ErrorMiddleware } from "./middlewares/error.middleware";
const app = express();
const port = 9000;
app.use(express.json());
// Payments route
app.post("/payments", (req: Request, res: Response) => {
const amount = +req.body.amount;
if (amount >= 100)
throw new InsufficientFundsError({
message: "Decline charge",
details: "Your available balance is less than your ledger balance"
});
else
res.send("Allow to take money out of account...");
});
// Orders route
app.get("/orders/:id", (req: Request, res: Response) => {
const id = +req.params.id;
if (!id)
throw new Error("Id not found");
else
res.send([]);
});
// Error handler
app.use(ErrorMiddleware.handle);
app.listen(port, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});
Let’s explain the server:
We have an Express server running on port 9000.
We have a POST route for making payments and a GET route for retrieving orders.
If the payment amount is greater than 100, we throw an InsufficientFundsError.
If the order ID is not provided, we throw a generic Error.
We have an error handler middleware to handle any errors thrown in the routes.
If an error is encountered, the error handler middleware will handle the error and send an appropriate http response.
Test API
Try to pay an order above 100
Remember “If the payment amount is greater than 100, we throw an Insufficient Funds Error”
curl -X POST \
'http://localhost:9000/payments' \
--header 'Accept: */*' \
--header 'User-Agent: SenorDeveloper (https://www.maxmartinez.dev)' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 200
}'
Response
Status: 400 Bad Request
{
"error": {
"name": "InsufficientFundsError",
"message": "Decline charge",
"details": "Your available balance is less than your ledger balance,",
"stack": [
"InsufficientFundsError:",
"at /Users/xxx/hashnode-blog/meaningful-exceptions/src/server.ts:12:15",
"at Layer.handle [as handle_request] (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/layer.js:95:5)",
"at next (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/route.js:149:13)",
"at Route.dispatch (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/route.js:119:3)",
"at Layer.handle [as handle_request] (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/layer.js:95:5)",
"at /Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:284:15",
"at Function.process_params (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:346:12)",
"at next (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:280:10)",
"at /Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/body-parser/lib/read.js:137:5",
"at AsyncResource.runInAsyncScope (node:async_hooks:206:9)"
]
}
}
The http response represents an error object with the following properties:
name: It indicates the type of error which is "InsufficientFundsError".
message: It describes the error message as "Decline charge".
details: It provides more information about the error, stating that the “available balance is less than the ledger balance”.
stack: It is an array that contains the stack trace of the error, showing the sequence of function calls that led to the error.
Retrieving an order with a not valid id
curl -X GET \
'http://localhost:9000/orders/0' \
--header 'Accept: */*' \
--header 'User-Agent: SenorDeveloper (https://www.maxmartinez.dev)' \
--header 'Content-Type: application/json'
Response
Status: 500 Internal Server Error
{
"error": {
"name": "InternalServerError",
"message": "Error",
"details": "Id not found",
"stack": [
"InternalServerError:",
"at handle (/Users/xxx/hashnode-blog/meaningful-exceptions/src/middlewares/error.middleware.ts:14:22)",
"at Layer.handle_error (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/layer.js:71:5)",
"at trim_prefix (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:326:13)",
"at /Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:286:9",
"at Function.process_params (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:346:12)",
"at next (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/index.js:280:10)",
"at next (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/route.js:141:14)",
"at Layer.handle [as handle_request] (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/layer.js:97:5)",
"at next (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/route.js:149:13)",
"at Route.dispatch (/Users/xxx/hashnode-blog/meaningful-exceptions/node_modules/express/lib/router/route.js:119:3)"
]
}
}
As the Orders route does not implement any custom exception it triggers a generic error: throw new Error("Id not found");
for this case, the error middleware identify that the Error is not an instance of BaseError and creates a new instance of InternalServerError using the original error message.
Conclusion
A key component of software development that enables programmers to deal with mistakes and unexpected situations is exception handling. But many developers frequently forget how important it is to use meaningful exceptions in their code. Instead, they are relying on generic error messages or not handling errors at all, which makes it challenging for them to precise the exact cause of the issues.
Pros of using meaningful exception names
Improved code readability and maintainability
Quicker debugging and troubleshooting
Enhanced team communication
Assistance in identifying and resolving possible issues more quickly
Cons of using meaningful exception names
Additional work required to define and manage custom error classes
Potential overkill for straightforward or small-scale applications
Possibly revealing vulnerabilities that can be exploited.
What we should not do
Do not expose detailed error messages to end-users. This can potentially expose sensitive information and increase the risk of security breaches.
For security validation, don't depend just on error notifications. Error messages can help attackers exploit vulnerabilities, thus they shouldn't be utilised to validate user input or give them feedback.
Don't overlook proper error handling and logging practices. Strong error-handling procedures must be in place to safeguard the application's security and stop sensitive data from being revealed.
Finally, meaningful exception names can improve communication within a development team or with external users and, also improve information that we can provide for logging practices. However, exposing detailed data about errors through meaningful exception names can pose several security risks.
Now that you’ve read this article, you know exactly how to deal with exceptions more effectively and what are the drawbacks of exposing detailed errors.
Subscribe to my newsletter and don't miss new articles.
See you then.