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/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);
}
πΉ 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)