Back to all posts

Effective Error Handling Without Messy Code

S
sonhp
10 min read

Effective Error Handling Without Messy Code

Error handling is critical in any application, but if done incorrectly, it can lead to spaghetti code, unnecessary complexity, and poor maintainability.

In this guide, we’ll cover:

  • Why error handling is important
  • Common bad practices
  • The global exception filter in NestJS
  • Using custom error classes
  • Handling async/await errors properly
  • Best practices for API error responses

1. Why Clean Error Handling Matters

A poorly handled error can:
Crash your application unexpectedly
❌ Make debugging harder
❌ Create nested try/catch blocks everywhere
❌ Lead to inconsistent error messages

A well-structured approach:
Keeps the code clean
Prevents unnecessary crashes
Standardizes error messages
Improves debugging & logging


2. Common Bad Practices in Error Handling

Catching Errors Without Handling Them

try {
  const user = await this.userService.findUser(id);
} catch (error) {
  // Do nothing
}

🚨 Problem: The error is swallowed, making debugging difficult.


Using Generic Error Messages

throw new Error('Something went wrong');

🚨 Problem: No context is provided for debugging.


Overusing try/catch Blocks Everywhere

try {
  const user = await this.userService.findUser(id);
  try {
    const orders = await this.orderService.getUserOrders(user.id);
  } catch (error) {
    console.error(error);
  }
} catch (error) {
  console.error(error);
}

🚨 Problem: Creates deep nesting, making the code unreadable.


3. Using NestJS Exception Filters (Global Error Handling)

NestJS provides a built-in way to handle errors globally using exception filters.

📌 Create a Global Exception Filter

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  InternalServerErrorException,
} from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    if (exception instanceof HttpException) {
      const status = exception.getStatus();
      const message = exception.getResponse();
      response.status(status).json({ success: false, message });
    } else {
      console.error(exception);
      response
        .status(500)
        .json({ success: false, message: 'Internal server error' });
    }
  }
}

📌 Apply It in main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './filters/global-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new GlobalExceptionFilter());
  await app.listen(3000);
}
bootstrap();

Now, all uncaught exceptions will be handled globally, reducing unnecessary try/catch blocks!


4. Creating Custom Error Classes

Instead of using throw new Error(), define specific error classes.

📌 Create CustomException.ts

import { HttpException, HttpStatus } from '@nestjs/common';

export class NotFoundException extends HttpException {
  constructor(resource: string) {
    super(`${resource} not found`, HttpStatus.NOT_FOUND);
  }
}

export class ValidationException extends HttpException {
  constructor(errors: string[]) {
    super({ message: 'Validation failed', errors }, HttpStatus.BAD_REQUEST);
  }
}

📌 Use Them in Your Service

if (!user) {
  throw new NotFoundException('User');
}
if (!isValid(data)) {
  throw new ValidationException(['Email is required', 'Password too short']);
}

This improves clarity and standardizes error messages.


5. Handling Async/Await Errors Properly

Bad: Using try/catch Everywhere

try {
  const user = await this.userService.findUser(id);
  return user;
} catch (error) {
  console.error(error);
  throw new InternalServerErrorException();
}

Good: Letting NestJS Handle It

async getUser(id: string) {
  const user = await this.userService.findUser(id);
  if (!user) throw new NotFoundException('User');
  return user;
}

🚀 NestJS automatically catches unhandled exceptions, so unnecessary try/catch is avoided!


6. Best Practices for API Error Responses

Consistent Error Response Structure

All errors should return structured JSON responses:

{
  "success": false,
  "message": "Validation failed",
  "errors": ["Email is required", "Password too short"]
}

Improves API documentation
Easier for frontend teams to handle errors


7. Summary: Writing Clean Error Handling in NestJS

Key Takeaways

Use global exception filters to handle errors centrally
Create custom error classes instead of generic errors
Avoid deep try/catch nesting—let NestJS handle async errors
Standardize API error responses for better debugging

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles