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;
Let me explain one by one.
The first line:
type ValidValueType<T> = [T, undefined?];
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];
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;
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
}
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
}
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]
}
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]
}
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)