Typescript Meaningful Exception Handling

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.
Search for a command to run...

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.
Quilla Frontend Kit turns recurring API, authentication, and server-state concerns into composable TypeScript packages.

Quilla Backend Kit provides the foundations for structured TypeScript services while leaving the application architecture in your hands.

Practical constraints for preserving the framework’s boundaries, dependency direction, and readability as a test suite grows.

A tool-agnostic approach to separating test intent from UI mechanics as an automation suite grows.

In this article, we will go through the entire process of set up Visual Studio Code for debugging “Deno” programs using “V8 Inspector Protocol”. Step-by-step, we will be learning how to configure VSCode to debug a Deno project. If you don’t know Deno...

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.
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
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
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/**"],
}
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;
};
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.
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.
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,
},
});
}
}
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.
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.
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
}'
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.
curl -X GET \
'http://localhost:9000/orders/0' \
--header 'Accept: */*' \
--header 'User-Agent: SenorDeveloper (https://www.maxmartinez.dev)' \
--header 'Content-Type: application/json'
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.
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.
Improved code readability and maintainability
Quicker debugging and troubleshooting
Enhanced team communication
Assistance in identifying and resolving possible issues more quickly
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.
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.