Overview
Use typed error classes from @/lib/errors for consistent error responses. The handleError() wrapper logs errors and returns properly formatted responses.
What it is
A set of error classes that map to HTTP status codes, plus a wrapper function that handles logging and response formatting.
Why we use it
Consistent error responses across all endpoints, automatic logging, and type-safe error creation.
When to use
Any API route or server action that can fail. Throw typed errors, let handleError() format the response.
Key Features
- Typed error classes for common HTTP errors
- handleError() wrapper for consistent responses
- Automatic error logging with context
- Client-safe error messages (no internal details)
Quick Start
Basic Error Handling
Throwing and handling errors in an API route.
// Using error classes with handleError()
import { handleError, NotFoundError, ValidationError } from '@/lib/errors';
export async function GET(request: Request) {
try {
const user = await findUser(id);
if (!user) {
throw new NotFoundError('User not found');
}
return NextResponse.json({ ok: true, data: user });
} catch (error) {
return handleError(error);
}
}Patterns
Error Classes
Available error classes and their HTTP status codes.
// Available error classes
import {
ApiError, // Base class (500)
ValidationError, // 400 Bad Request
AuthenticationError,// 401 Unauthorized
AuthorizationError, // 403 Forbidden
NotFoundError, // 404 Not Found
ConflictError, // 409 Conflict
} from '@/lib/errors';
// Usage
throw new ValidationError('Email format is invalid');
throw new NotFoundError('Project not found');
throw new AuthorizationError('Insufficient permissions');Using handleError()
The standard pattern for catching and returning errors.
// handleError() does three things:
// 1. Logs the error with context
// 2. Returns proper HTTP status code
// 3. Formats consistent error response
export async function POST(request: Request) {
try {
// Your logic here
const result = await someOperation();
return NextResponse.json({ ok: true, data: result });
} catch (error) {
// This handles logging + response formatting
return handleError(error);
}
}
// Output for ValidationError:
// Status: 400
// Body: { ok: false, error: { code: 'VALIDATION_ERROR', message: '...' } }Creating Custom Errors
Extending ApiError for domain-specific errors.
// Creating domain-specific errors
import { ApiError } from '@/lib/errors';
export class RateLimitError extends ApiError {
constructor(message = 'Too many requests') {
super(message, 429, 'RATE_LIMIT_EXCEEDED');
}
}
export class PaymentError extends ApiError {
constructor(message: string) {
super(message, 402, 'PAYMENT_REQUIRED');
}
}Watch Out
Exposing internal error details to clients
Don't
// Exposing internal details
return NextResponse.json({
error: error.stack, // Dangerous!
message: error.message
});Do
// Using handleError for safe responses
return handleError(error);
// Returns: { ok: false, error: { code, message } }
// Internal details are logged, not exposedNot logging errors before returning
Don't
// Silent errors
catch (error) {
return NextResponse.json({ error: 'Something went wrong' });
}Do
// handleError logs before returning
catch (error) {
return handleError(error);
// Logs: { level: 'error', message, stack, context }
}- Using wrong HTTP status codes
- Using generic messages that don't help debugging