Pular para o conteúdoPedro Farbo
Lição 7 / 2535 min

Tratamento de Erros Profissional

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! 🗄️

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e