DEV Community

Shoki Ishii
Shoki Ishii

Posted on

Type-Safe Backends with TypeScript: Use Custom Error Handling

Introduction

When working with TypeScript in backend applications (I use next.js), ensuring type safety is important. It not only makes your code less prone to errors but also enhances readability and maintainability. By leveraging TypeScript's advanced type features, we can create a more structured and type-safe error-handling pattern. In this article, I will explore a custom approach to error handling focusing on the backend.

Understanding the Proposed Type

This is the error handling type that I am using in my project.

type ValidValueType<T> = [T, undefined?];
type ErrorValueType = [undefined, Error];
export type ReturnValueType<T> = ValidValueType<T> | ErrorValueType;
Enter fullscreen mode Exit fullscreen mode

Let me explain one by one.

The first line:

type ValidValueType<T> = [T, undefined?];
Enter fullscreen mode Exit fullscreen mode

The first value is the assigned value of generic type T.
The second value is optional and can be undefined.

The second line:

type ErrorValueType = [undefined, Error];
Enter fullscreen mode Exit fullscreen mode

The first value is undefined, indicating the absence of a successful return value.
The second value is an Error object, carrying details about the error that occurred.

The third line:

export type ReturnValueType<T> = ValidValueType<T> | ErrorValueType;
Enter fullscreen mode Exit fullscreen mode

By combining these two types into ReturnValueType, we create a union type that can handle both successful and error returns in a structured manner.

Implementing the Custom Error Handling

Step 1: Setting Up the Function

Let's see the code example to know how it can be useful.
This is a simple function without error handling that can fetch User. (Non-essential parts are omitted for clarity.)

interface User {
 id: number;
 name: string;
// ...
}

const handler = async(req: Request, res: Response) => {
 const user = await retrieveUser(req.query.id);
}

const retrieveUser = async(id: number) => {
 const user = await findUser<User>(query); // find user with id
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Use the Custom Error Handling

Add the custom error type to the function.
In this case, I've added Promise> to retrieveUser.

interface User {
 id: number;
 name: string;
// ...
}

const handler = async(req: Request, res: Response) => {
 const [user, error] = await retrieveUser(req.query.id);
}

const retrieveUser = async(id: number): Promise<ReturnValueType<User>> => {
 const user = await findUser<User>(query); // find user with id
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Return the User to the handler

We only can return the User object thanks to the custom return type.

interface User {
 id: number;
 name: string;
// ...
}

const handler = async(req: Request, res: Response) => {
 const [user, error] = await retrieveUser(req.query.id);
}

const retrieveUser = async(id: number): Promise<ReturnValueType<User>> => {
 const user = await findUser<User>(query); // find user with id

 if(!user) {
 return [undefined, { name :"NotFound", message: `UserId ${id} was not found` }]
 }

 return [User]
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Return the response to the client

Finally, we just return the response to the client.

interface User {
 id: number;
 name: string;
// ...
}

const handler = async(req: Request, res: Response) => {
 const [user, error] = await retrieveUser(req.query.id);
 if(error.name === "NotFound") {
   return res.status(404).json(error.message)
 }
 if(error) {
   return res.status(500).json(error.message)
 }

 return res.status(200).json(user)
}

const retrieveUser = async(id: number): Promise<ReturnValueType<User>> => {
 const user = await findUser<User>(query); // find user with id

 if(!user) {
 return [undefined, { name :"NotFound", message: `UserId ${id} was not found` }]
 }

 return [User]
}
Enter fullscreen mode Exit fullscreen mode

Through this process, we efficiently handle both successful responses and different types of errors, returning appropriate HTTP status codes and messages to the client. This approach not only makes the code more readable but also enhances the reliability of error handling in the application.

This is still a very simple implementation and you can customize more as you need. The real power of this approach lies in its customizability, adding metadata, customizing error types, and more.

Happy Coding!

Top comments (0)