Tratamento de Erros Profissional
Um bom tratamento de erros diferencia APIs amadoras de APIs profissionais. Nesta aula, vamos implementar um sistema robusto de error handling.
Tipos de Erros
1. Erros Operacionais (Esperados)
- Usuário não encontrado (404)
- Email já cadastrado (409)
- Validação falhou (400)
- Não autorizado (401)
2. Erros de Programação (Bugs)
- TypeError, ReferenceError
- Sintaxe incorreta
- Null pointer exceptions
Classes de Erro Customizadas
typescript
// src/errors/AppError.tsexport class AppError extends Error { public readonly statusCode: number; public readonly isOperational: boolean; public readonly code?: string; constructor( message: string, statusCode: number = 500, code?: string, isOperational: boolean = true ) { super(message); this.statusCode = statusCode; this.isOperational = isOperational; this.code = code; Object.setPrototypeOf(this, AppError.prototype); Error.captureStackTrace(this, this.constructor); }}Erros Específicos
typescript
// src/errors/index.tsimport { AppError } from './AppError'; export class NotFoundError extends AppError { constructor(resource: string = 'Recurso') { super(`${resource} não encontrado`, 404, 'NOT_FOUND'); }} export class ValidationError extends AppError { public readonly errors: Array<{ field: string; message: string }>; constructor(errors: Array<{ field: string; message: string }>) { super('Dados inválidos', 400, 'VALIDATION_ERROR'); this.errors = errors; }} export class UnauthorizedError extends AppError { constructor(message: string = 'Não autorizado') { super(message, 401, 'UNAUTHORIZED'); }} export class ForbiddenError extends AppError { constructor(message: string = 'Acesso negado') { super(message, 403, 'FORBIDDEN'); }} export class ConflictError extends AppError { constructor(message: string) { super(message, 409, 'CONFLICT'); }} export class TooManyRequestsError extends AppError { constructor(retryAfter: number) { super(`Muitas requisições. Tente novamente em ${retryAfter}s`, 429, 'TOO_MANY_REQUESTS'); }} export { AppError };Error Handler Global
typescript
// src/middlewares/errorHandler.middleware.tsimport { Request, Response, NextFunction } from 'express';import { ZodError } from 'zod';import { AppError, ValidationError } from '../errors'; interface ErrorResponse { success: false; message: string; code?: string; errors?: Array<{ field: string; message: string }>; stack?: string;} export function errorHandler( err: Error, req: Request, res: Response, next: NextFunction): void { // Log do erro console.error(`[${new Date().toISOString()}] Error:`, { name: err.name, message: err.message, stack: err.stack, path: req.path, method: req.method, requestId: req.requestId, }); // Resposta base const response: ErrorResponse = { success: false, message: 'Erro interno do servidor', }; let statusCode = 500; // AppError - Erros operacionais if (err instanceof AppError) { statusCode = err.statusCode; response.message = err.message; response.code = err.code; if (err instanceof ValidationError) { response.errors = err.errors; } } // Zod Validation Error else if (err instanceof ZodError) { statusCode = 400; response.message = 'Dados inválidos'; response.code = 'VALIDATION_ERROR'; response.errors = err.errors.map(e => ({ field: e.path.join('.'), message: e.message, })); } // JWT Errors else if (err.name === 'JsonWebTokenError') { statusCode = 401; response.message = 'Token inválido'; response.code = 'INVALID_TOKEN'; } else if (err.name === 'TokenExpiredError') { statusCode = 401; response.message = 'Token expirado'; response.code = 'TOKEN_EXPIRED'; } // Prisma Errors else if (err.name === 'PrismaClientKnownRequestError') { const prismaError = err as any; if (prismaError.code === 'P2002') { statusCode = 409; response.message = 'Registro já existe'; response.code = 'DUPLICATE_ENTRY'; } else if (prismaError.code === 'P2025') { statusCode = 404; response.message = 'Registro não encontrado'; response.code = 'NOT_FOUND'; } } // Incluir stack em desenvolvimento if (process.env.NODE_ENV !== 'production') { response.stack = err.stack; } res.status(statusCode).json(response);}Usando os Erros
typescript
// src/services/user.service.tsimport { NotFoundError, ConflictError, ForbiddenError } from '../errors'; export class UserService { async findById(id: string): Promise<UserResponse> { const user = await this.userRepository.findById(id); if (!user) { throw new NotFoundError('Usuário'); } return this.toUserResponse(user); } async create(data: CreateUserDTO): Promise<UserResponse> { const existingUser = await this.userRepository.findByEmail(data.email); if (existingUser) { throw new ConflictError('Email já cadastrado'); } // ... } async delete(id: string, requestingUserId: string): Promise<void> { if (id === requestingUserId) { throw new ForbiddenError('Você não pode deletar sua própria conta'); } const deleted = await this.userRepository.delete(id); if (!deleted) { throw new NotFoundError('Usuário'); } }}Async Handler
typescript
// src/utils/asyncHandler.tsimport { Request, Response, NextFunction, RequestHandler } from 'express'; type AsyncRequestHandler = ( req: Request, res: Response, next: NextFunction) => Promise<any>; export function asyncHandler(fn: AsyncRequestHandler): RequestHandler { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };}Respostas Padronizadas
typescript
// src/utils/responses.tsexport interface ApiResponse<T = unknown> { success: boolean; message?: string; data?: T; meta?: { page?: number; limit?: number; total?: number; totalPages?: number; };} export function success<T>(data: T, message?: string): ApiResponse<T> { return { success: true, data, message };} export function paginated<T>( data: T[], page: number, limit: number, total: number): ApiResponse<T[]> { return { success: true, data, meta: { page, limit, total, totalPages: Math.ceil(total / limit), }, };}Configuração Final
typescript
// src/app.tsimport express from 'express';import cors from 'cors';import helmet from 'helmet';import { errorHandler } from './middlewares/errorHandler.middleware';import { requestIdMiddleware } from './middlewares/requestId.middleware';import routes from './routes'; const app = express(); // Middlewares de segurançaapp.use(helmet());app.use(cors()); // Request IDapp.use(requestIdMiddleware); // Body parsersapp.use(express.json()); // Rotasapp.use('/api', routes); // 404 handlerapp.use((req, res) => { res.status(404).json({ success: false, message: 'Rota não encontrada', code: 'ROUTE_NOT_FOUND', });}); // Error handler (sempre por último!)app.use(errorHandler); export default app;Resumo
- ✅ Classes de erro customizadas e tipadas
- ✅ Error handler global que trata diferentes tipos
- ✅ Respostas padronizadas e consistentes
- ✅ AsyncHandler para capturar erros em rotas async
- ✅ Logs estruturados para debugging
Na próxima aula: Introdução a Bancos de Dados! 🗄️