DEV Community

Cover image for Exception Handling Overview
Taki
Taki

Posted on

Exception Handling Overview

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');
Enter fullscreen mode Exit fullscreen mode
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(),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Then use it globally:

// main.ts
app.useGlobalFilters(new AllExceptionsFilter());
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 3. Best Practices for NestJS

  • ❗ Throw, don’t return errors: throw new BadRequestException(...)
  • βœ… Centralize error formatting with global filter
  • 🎯 Use try/catch in 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);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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' });
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”Ή 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then wrap your component:

<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>
Enter fullscreen mode Exit fullscreen mode

🚨 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;
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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');
  }
}
Enter fullscreen mode Exit fullscreen mode

You can now throw meaningful errors:

if (!user) throw new ResourceNotFoundException('User');
Enter fullscreen mode Exit fullscreen mode

βœ… 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,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

βž• Register Globally

// main.ts
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
app.useGlobalFilters(new GlobalExceptionFilter());
Enter fullscreen mode Exit fullscreen mode

πŸ§‘β€πŸŽ¨ 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;
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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();
}
Enter fullscreen mode Exit fullscreen mode

βœ… 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>;
}
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ 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');
  }
}));
Enter fullscreen mode Exit fullscreen mode

βœ… 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)