Handling exceptions effectively is crucial for writing robust, maintainable, and secure applications, especially in a fullstack architecture like Next.js (frontend) and NestJS (backend).
π GENERAL CONCEPTS
β What is Exception Handling?
Exception handling is the process of catching errors at runtime and responding to them gracefully without crashing the app.
π₯ CORE THINGS YOU NEED TO KNOW
1. Types of Errors
| Type | Description | Example | 
|---|---|---|
| Syntax Error | Code canβt run | Missing bracket if (...) {
 | 
| Runtime Error | Code crashes while running | Null reference | 
| Logic Error | Code runs but wrong result | Wrong conditional check | 
π¦ NESTJS β BACKEND EXCEPTION HANDLING
NestJS is built on Express (or optionally Fastify), and has powerful tools for error handling.
πΉ 1. Use Built-in Exceptions
NestJS provides a set of HTTP exception classes.
import { BadRequestException, NotFoundException } from '@nestjs/common';
throw new BadRequestException('Invalid input');
| Exception | HTTP Code | 
|---|---|
BadRequestException | 
400 | 
UnauthorizedException | 
401 | 
ForbiddenException | 
403 | 
NotFoundException | 
404 | 
InternalServerErrorException | 
500 | 
  
  
  πΉ 2. Use Filters (@Catch) for Custom Handling
import {
  ExceptionFilter, Catch, ArgumentsHost, HttpException, Logger,
} from '@nestjs/common';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : 500;
    const message = exception instanceof HttpException
      ? exception.getResponse()
      : 'Internal server error';
    Logger.error(`[Exception]: ${JSON.stringify(message)}`);
    response.status(status).json({
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
    });
  }
}
Then use it globally:
// main.ts
app.useGlobalFilters(new AllExceptionsFilter());
πΉ 3. Best Practices for NestJS
- β Throw, donβt return errors: 
throw new BadRequestException(...) - β Centralize error formatting with global filter
 - π― Use 
try/catchin services if working with external APIs or DB 
βοΈ NEXT.JS β FRONTEND EXCEPTION HANDLING
πΉ 1. Handle Client-Side Errors
try {
  const res = await fetch('/api/data');
  if (!res.ok) throw new Error('Failed to fetch');
} catch (err) {
  console.error(err.message);
}
  
  
  πΉ 2. Handle API Route Errors (e.g., /api/xyz)
export default async function handler(req, res) {
  try {
    // logic here
    res.status(200).json({ data: 'ok' });
  } catch (err) {
    console.error('API Error:', err);
    res.status(500).json({ error: 'Internal Server Error' });
  }
}
πΉ 3. Use Error Boundaries for UI Crashes
// ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    console.error('Error caught in boundary:', error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      return <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}
Then wrap your component:
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>
π¨ CATEGORIES OF EXCEPTIONS TO HANDLE
| Category | Examples | Layer | 
|---|---|---|
| Validation | Missing fields, wrong formats | Backend | 
| Auth | JWT expired, unauthorized | Both | 
| External API | Timeout, invalid key | Backend | 
| Server Crash | Unhandled exception | Backend | 
| UI Error | Render crash | Frontend | 
| Network | Fetch fail, offline | Frontend | 
βοΈ TOOLING TIPS
- β Use Zod or class-validator for schema/DTO validation
 - β Integrate Sentry for real-time exception logging
 - β Use Axios interceptors (client + server) to standardize error format
 - β Write unit tests to simulate exceptions
 
π SECURITY ADVICE
- β Never expose raw errors or stack traces to users
 - β Mask sensitive error messages (e.g., DB error β βSomething went wrongβ)
 - β Log full details internally with request context
 
β SUMMARY CHECKLIST
| Area | What to Ensure | 
|---|---|
| β Throw exceptions properly in NestJS | Use built-in HttpException classes | 
| β Format all errors in consistent structure | Use ExceptionFilter
 | 
| β Handle both client-side and API fetch errors in Next.js | Try/catch, .ok checks | 
| β Prevent UI crashes | Use <ErrorBoundary />
 | 
| β Add observability | Use logs, alerts, tools like Sentry | 
| β Protect from info leaks | Sanitize messages shown to user | 
βοΈ PART 1: ERROR HANDLING PATTERN OVERVIEW
We'll standardize errors across the entire stack using the following pattern:
π§± Standard Error Structure
Every error from backend should follow a consistent shape:
interface AppErrorResponse {
  statusCode: number;
  error: string; // e.g., "Bad Request"
  message: string | string[];
  timestamp: string;
  path?: string;
}
This will ensure the frontend can expect and format errors consistently.
π§ PART 2: NESTJS CUSTOM EXCEPTION PATTERN
β 1. Base App Exception Class
Create a base exception to extend:
// src/common/exceptions/app.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class AppException extends HttpException {
  constructor(message: string, statusCode: number = HttpStatus.INTERNAL_SERVER_ERROR, error = 'Application Error') {
    super(
      {
        statusCode,
        message,
        error,
        timestamp: new Date().toISOString(),
      },
      statusCode,
    );
  }
}
β 2. Custom Exception Examples
// src/common/exceptions/invalid-input.exception.ts
import { AppException } from './app.exception';
import { HttpStatus } from '@nestjs/common';
export class InvalidInputException extends AppException {
  constructor(message = 'Invalid input provided') {
    super(message, HttpStatus.BAD_REQUEST, 'Bad Request');
  }
}
// src/common/exceptions/resource-not-found.exception.ts
import { AppException } from './app.exception';
import { HttpStatus } from '@nestjs/common';
export class ResourceNotFoundException extends AppException {
  constructor(resource = 'Resource') {
    super(`${resource} not found`, HttpStatus.NOT_FOUND, 'Not Found');
  }
}
You can now throw meaningful errors:
if (!user) throw new ResourceNotFoundException('User');
β 3. Global Exception Filter (Formatter)
// src/common/filters/global-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
@Catch()
export class GlobalExceptionFilter implements 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 errorResponse =
      exception instanceof HttpException
        ? exception.getResponse()
        : {
            statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
            message: 'Internal server error',
            error: 'Internal Server Error',
          };
    const finalError =
      typeof errorResponse === 'string'
        ? {
            statusCode: status,
            message: errorResponse,
            error: 'Error',
          }
        : errorResponse;
    response.status(status).json({
      ...finalError,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}
β Register Globally
// main.ts
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
app.useGlobalFilters(new GlobalExceptionFilter());
π§βπ¨ PART 3: NEXT.JS FRONTEND PATTERN
β 1. Shared Error Interface
// types/ErrorResponse.ts
export interface AppErrorResponse {
  statusCode: number;
  message: string | string[];
  error: string;
  timestamp: string;
  path?: string;
}
β 2. Fetch Wrapper
// lib/fetcher.ts
import { AppErrorResponse } from '@/types/ErrorResponse';
export async function safeFetch<T>(url: string, options?: RequestInit): Promise<T> {
  const res = await fetch(url, options);
  if (!res.ok) {
    const err: AppErrorResponse = await res.json();
    throw err;
  }
  return res.json();
}
β 3. Example Usage
import { useEffect, useState } from 'react';
import { safeFetch } from '@/lib/fetcher';
import { AppErrorResponse } from '@/types/ErrorResponse';
export default function UserDetails() {
  const [error, setError] = useState<AppErrorResponse | null>(null);
  const [user, setUser] = useState<any>(null);
  useEffect(() => {
    safeFetch('/api/user/me')
      .then(setUser)
      .catch(setError);
  }, []);
  if (error) return <p className="text-red-600">β οΈ {error.message}</p>;
  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}
π¦ BONUS: DTO Validation Exception Integration
Use class-validator and map ValidationException to our structure:
// In validation pipe
app.useGlobalPipes(new ValidationPipe({
  exceptionFactory: (errors) => {
    const messages = errors.map(err => `${err.property} - ${Object.values(err.constraints).join(', ')}`);
    return new AppException(messages, 400, 'Validation Failed');
  }
}));
β Summary
| Component | Role | 
|---|---|
AppException | 
Base for custom exceptions | 
GlobalExceptionFilter | 
Standardize structure | 
safeFetch() | 
Ensure frontend receives predictable errors | 
AppErrorResponse | 
Shared error interface | 
class-validator β AppException
 | 
Hook DTO validation into custom errors | 
              
    
Top comments (0)