Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Stop Writing Brittle if/else Error Handlers in Node.js
Mark M
Mark M

Posted on

Stop Writing Brittle if/else Error Handlers in Node.js

YourerrorHandler.js is probably a mess. If you're building applications in Node.js with Express, you've likely ended up with a centralerrorHandler middleware that looks like a giant, brittle if/else if chain. It starts small, but soon it's a monster you're afraid to touch.

// The "Before" state we all know...if(errinstanceofBadRequestError){// ...}elseif(errinstanceofAuthError){// ...}elseif(errinstanceofPaymentError){// ...}elseif(errinstanceofAnotherCustomError){// You have to modify this file every time you add a new error type!}
Enter fullscreen modeExit fullscreen mode

This approach is tightly coupled and not scalable. Today, I'll show you how to refactor this into a flexible, type-safe, and polymorphic system that you'll never have to modify again.

The Goal: A Decoupled, Polymorphic Handler

Our goal is to create anerrorHandler that doesn't care about the specific class of an error. It only cares that the error conforms to a contract. This allows us to create new error types anywhere in our application without ever touching the central handler.

Step 1: Define the Contract with an Interface

First, we define our contract using a TypeScript interface. This is the single source of truth for what makes an error "handleable" by our system. We'll also define a utilityserializeError function here, which helps standardize how we log original error details.

// errors/common.tsimport{ZodIssue}from"zod";exportinterfaceIHandleableError{statusCode:number;formatResponse():{message:string;statusCode:number;userDetails?:ZodIssue[]};log():Record<string,any>;}exportfunctionserializeError(error?:unknown){if(errorinstanceofError){// Prevents issues with circular references in error objects and captures key details.return{message:error.message,type:error.constructor.name,name:error.name,stack:error.stack,};}returnerror;// Return as-is if not an Error instance (e.g., string, number, null)}
Enter fullscreen modeExit fullscreen mode

This contract guarantees three things:

  • statusCode: The HTTP status code to send back.
  • formatResponse(): A method that returns a user-safe object for the JSON response.
  • log(): A method that returns a rich object for internal, server-side logging.

Step 2: Create a Base Error Class

Next, we create aCustomError class that implements our new interface. This will serve as a convenient base for most of our other errors.

// errors/CustomError.tsimport{IHandleableError,serializeError}from"./common";exportclassCustomErrorextendsErrorimplementsIHandleableError{constructor(publicmessage:string,publicstatusCode:number,publicoriginalError:unknown,// ... other properties you might need){super(message);}formatResponse():{message:string;statusCode:number;userDetails?:ZodIssue[]}{return{message:this.message,statusCode:this.statusCode,};}log(){return{// Common details for logging across all custom errorsname:this.constructor.name,message:this.message,statusCode:this.statusCode,originalError:serializeError(this.originalError),};}}
Enter fullscreen modeExit fullscreen mode

Step 3: Extend Your Base for Specific Errors

Now you can create specific, meaningful errors by extending the base class.

For example, aBadRequestError can format its response to include detailed validation errors from a library like Zod.

// errors/BadRequestError.tsimport{ZodIssue}from"zod";import{CustomError}from"./CustomError";exportclassBadRequestErrorextendsCustomError{constructor(message:string,publicerrors?:ZodIssue[]){super(message,400,null);// 400 is the standard status code for Bad Request}// Override to provide specific, user-safe detailsformatResponse():{message:string;statusCode:number;userDetails:ZodIssue[]}{return{message:this.message,statusCode:this.statusCode,userDetails:this.errors||[],// Safely expose validation errors to the client};}log(){return{...super.log(),// Include base log detailsvalidationErrors:this.errors,// ... additional logging details specific to BadRequestError};}}
Enter fullscreen modeExit fullscreen mode

And for sensitive errors likeAuthError, you can ensure that internal details like theoriginalError are not logged for security reasons:

// errors/AuthError.tsimport{CustomError}from"./CustomError";exportclassAuthErrorextendsCustomError{constructor(message:string,publicoriginalError?:unknown){super(message,401,originalError);// 401 is the standard status code for Unauthorized}formatResponse():{message:string;statusCode:number;userDetails?:ZodIssue[]}{return{message:this.message,statusCode:this.statusCode,};}log(){// For security, explicitly override and ensure sensitive originalError is not logged.return{message:this.message,statusCode:this.statusCode,// Do NOT include originalError here for security};}}
Enter fullscreen modeExit fullscreen mode

Step 4: The New, SmartererrorHandler

This is the payoff. Our finalerrorHandler is now incredibly simple and powerful. It doesn't know aboutBadRequestError orAuthError; it only knows about theIHandleableError contract.

// middleware/errorHandler.tsimport{Request,Response,NextFunction}from"express";import{formatErrorResponse}from"./responseFormatter";// This is your provided utilityimport{logger}from"@/utils/logger";// Assuming you have a logger utilityimport{IHandleableError}from"@/errors/common";exportconsterrorHandler=(err:unknown,req:Request,res:Response,next:NextFunction)=>{// Basic request context for consistent loggingconstlogBase={method:req.method,url:req.originalUrl,user:req.user||null,};// This is the only check we need!// We check for Error instance and duck-type IHandleableError methods/properties.// This is because interfaces don't exist at runtime in TypeScript.if(errinstanceofError&&"formatResponse"inerr&&"log"inerr&&"statusCode"inerr){consthandleableError=errasIHandleableError;// Asserting the type for convenience// Differentiate logging for client (4xx) vs. server (5xx) errorsif(handleableError.statusCode<500){logger.warn({message:err.message,...logBase,...handleableError.log()});}else{logger.error({message:err.message,...logBase,...handleableError.log()});}res.status(handleableError.statusCode).json(formatErrorResponse(handleableError.formatResponse()));return;}// Fallback for completely unknown or unhandled errorsconstunknownError=errinstanceofError?err:newError("Unknown server error occurred.");logger.error({...logBase,message:unknownError.message,stack:unknownError.stack,// Add any other details for truly unexpected errors});res.status(500).json(formatErrorResponse({message:"Internal Server Error",statusCode:500}));};
Enter fullscreen modeExit fullscreen mode

Step 5: The Response Formatter Utility

While not strictly part of the error handling logic, having a consistent way to format API responses, especially errors, is crucial. This utility ensures all your error responses adhere to a unified structure.

// utils/responseFormatter.ts (or wherever you prefer)import{ErrorResponse,Pagination,SuccessResponse}from"@/types/response.types";import{ZodIssue}from"zod";// Generic success response formatter (included for context)exportfunctionformatResponse<T>(data:T,pagination?:Pagination,):SuccessResponse<T>{return{success:true,data,pagination,};}// Error response formatterexportfunctionformatErrorResponse({message,statusCode,userDetails,}:{message:string;statusCode:number;userDetails?:ZodIssue[];}):ErrorResponse{return{success:false,error:{message,code:statusCode,details:userDetails,},};}
Enter fullscreen modeExit fullscreen mode

And that's it. You can now create dozens of different error types that all implementIHandleableError, and you willnever have to touch this file again.

Key Takeaways

This refactored approach gives you a system that is:

  • Decoupled: The central handler isn't tied to specific error classes.
  • Scalable: Add new error types with zero friction.
  • Type-Safe: The interface acts as a compile-time contract, preventing mistakes.
  • Maintainable: Your logic is clean, simple, and easy to reason about.
  • Secure: Granular control over logging allows you to protect sensitive information on a per-error basis.

Stop fighting with brittle error handlers and adopt a polymorphic, contract-based approach. Your future self will thank you.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Full-stack developer in San Jose building projects and writing dev logs about what actually breaks and how I fix it. If you're building on your own, you'll likely find your struggles mirrored in mine.
  • Location
    San Jose, CA
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp