Lesson 46 of 49 intermediate

NestJS Exception Filters & Error Handling

Catching Errors Like a Pro

Open interactive version (quiz + challenge)

Real-world analogy

Exception filters are like an emergency room in a hospital. When something goes wrong with a patient (an error in your code), instead of panicking in the hallway, they are routed to the ER (filter) where trained staff handle the situation calmly, document what happened, and communicate clearly with the family (client). Without an ER, every emergency would be chaos.

What is it?

Exception filters catch errors thrown during request processing and convert them into appropriate HTTP responses. NestJS provides built-in HTTP exceptions for common status codes, a default global filter, and the ability to create custom filters for specialized error handling.

Real-world relevance

Every production API needs consistent error handling. Stripe returns structured error objects with codes, messages, and docs links. GitHub API uses consistent error formats across hundreds of endpoints. Exception filters make this level of consistency effortless.

Key points

Code example

// 1. Throwing built-in exceptions
import {
  NotFoundException, BadRequestException,
  ConflictException, ForbiddenException,
} from '@nestjs/common';

@Injectable()
export class UsersService {
  async findOne(id: number) {
    const user = await this.prisma.user.findUnique({ where: { id } });
    if (!user) {
      throw new NotFoundException(`User #${id} not found`);
    }
    return user;
  }

  async create(dto: CreateUserDto) {
    const exists = await this.prisma.user.findUnique({
      where: { email: dto.email },
    });
    if (exists) {
      throw new ConflictException('Email already registered');
    }
    return this.prisma.user.create({ data: dto });
  }
}

// 2. Custom Exception Filter — catch ALL HTTP errors
import {
  ExceptionFilter, Catch, ArgumentsHost,
  HttpException, HttpStatus, Logger,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger('ExceptionFilter');

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error';

    // Log the error with context
    this.logger.error(
      `${request.method} ${request.url} — ${status}`,
      exception instanceof Error ? exception.stack : '',
    );

    response.status(status).json({
      success: false,
      statusCode: status,
      message: typeof message === 'string'
        ? message
        : (message as any).message || message,
      path: request.url,
      timestamp: new Date().toISOString(),
    });
  }
}

// 3. Custom domain exception
export class UserNotFoundException extends NotFoundException {
  constructor(userId: number) {
    super({
      code: 'USER_NOT_FOUND',
      message: `User #${userId} not found`,
      userId,
    });
  }
}

// Usage: throw new UserNotFoundException(123);

// 4. Apply globally in main.ts
// app.useGlobalFilters(new AllExceptionsFilter());

// 5. Custom validation error format
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,
  transform: true,
  exceptionFactory: (errors) => {
    const formatted = errors.map(err => ({
      field: err.property,
      errors: Object.values(err.constraints || {}),
    }));
    return new BadRequestException({
      code: 'VALIDATION_ERROR',
      message: 'Validation failed',
      errors: formatted,
    });
  },
}));

Line-by-line walkthrough

  1. 1. 1. Throwing built-in exceptions
  2. 2. Import NestJS exception classes
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7. Injectable service class
  8. 8. Find a single user by ID
  9. 9. Query the database
  10. 10. If user doesn't exist...
  11. 11. Throw 404 with a descriptive message
  12. 12. Close if
  13. 13. Return the found user
  14. 14. Close method
  15. 15.
  16. 16. Create a new user
  17. 17. Check if email already exists
  18. 18. Query by email
  19. 19. Close query
  20. 20. If user exists with this email...
  21. 21. Throw 409 Conflict error
  22. 22. Close if
  23. 23. Create the user in the database
  24. 24. Close method
  25. 25. Close class
  26. 26.
  27. 27. 2. Custom Exception Filter — catch ALL errors
  28. 28. Import filter interfaces
  29. 29.
  30. 30.
  31. 31.
  32. 32.
  33. 33. @Catch() with no argument = catch everything
  34. 34. Implementing ExceptionFilter interface
  35. 35. Logger for error reporting
  36. 36.
  37. 37. catch method — exception and host
  38. 38. Switch to HTTP context
  39. 39. Get the response object
  40. 40. Get the request object
  41. 41.
  42. 42. Determine status code
  43. 43. If it's an HttpException, use its status
  44. 44. Otherwise, default to 500
  45. 45.
  46. 46. Determine error message
  47. 47. If HttpException, get its response
  48. 48. Otherwise, generic message
  49. 49.
  50. 50. Log the error with context
  51. 51. Format: GET /users — 404
  52. 52. Include stack trace for real errors
  53. 53. Close logger call
  54. 54.
  55. 55. Send the response
  56. 56. success: false flag
  57. 57. The HTTP status code
  58. 58. Extract the message string
  59. 59. Handle both string and object messages
  60. 60. Include the request path
  61. 61. Include a timestamp
  62. 62. Close json response
  63. 63. Close catch method
  64. 64. Close class
  65. 65.
  66. 66. 3. Custom domain exception
  67. 67. Extend NotFoundException for specific case
  68. 68. Constructor takes userId
  69. 69. Call parent with structured error object
  70. 70. Custom error code
  71. 71. Descriptive message
  72. 72. Include the userId for debugging
  73. 73. Close super call
  74. 74. Close constructor
  75. 75. Close class
  76. 76.
  77. 77. Usage: throw with specific user ID
  78. 78.
  79. 79. 4. Apply globally in main.ts
  80. 80.
  81. 81. 5. Custom validation error format
  82. 82. Configure ValidationPipe with custom factory
  83. 83. Standard options
  84. 84.
  85. 85. Custom error formatter
  86. 86. Map each validation error
  87. 87. Include the field name
  88. 88. Include all constraint messages
  89. 89. Close map
  90. 90. Return a structured BadRequestException
  91. 91. Custom error code
  92. 92. Clear message
  93. 93. Field-level errors
  94. 94. Close return
  95. 95. Close factory
  96. 96. Close pipe config

Spot the bug

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception.getStatus();

    response.json({
      statusCode: status,
      message: exception.message,
    });
  }
}
Need a hint?
The response is being sent but something is missing. What should you set before sending JSON?
Show answer
The filter sends JSON but never sets the HTTP status code on the response! It will always return 200 even for 404 or 500 errors. Fix: change response.json() to response.status(status).json(). Without .status(status), the browser/client thinks everything is fine!

Explain like I'm 5

You know when you play a video game and you make a mistake? Instead of the game crashing, it shows 'Game Over' with your score. Exception filters are like that — when your code makes a mistake, instead of crashing everything, the filter catches it and shows a nice error message like 'Sorry, we couldn't find that!'

Fun fact

HTTP status codes were defined in 1999 by RFC 2616. The famous 404 Not Found was supposedly named after Room 404 at CERN, where the original web servers were located. When people couldn't find a page, they'd say 'check room 404' — and the name stuck!

Hands-on challenge

Create an AllExceptionsFilter that: 1) Logs errors with request details, 2) Returns a consistent JSON format with statusCode, message, code, path, and timestamp, 3) Handles both HttpExceptions and unknown errors, 4) Create a custom DuplicateEmailException class. Apply the filter globally and test with different error scenarios!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook