Error Handling em TypeScript
Baixar PDFTratar erros como cidadãos de primeira classe — Result types, custom errors, narrowing de exceções em frontend e backend.
Error Handling em TypeScript
Em JavaScript, erros são invisíveis até o momento em que o código quebra em produção. TypeScript não muda esse comportamento em runtime — mas ele te dá ferramentas para tornar erros explícitos no sistema de tipos.
Os dois mundos do error handling
| try/catch (exceções) | Result types (valores) | |
|---|---|---|
| Como funciona | Erro "sobe" a call stack até ser capturado | Erro é um valor retornado, como qualquer outro |
| Visibilidade | Invisível na assinatura da função | Visível no tipo de retorno |
| Quando usar | Erros inesperados (rede caiu, disco cheio) | Erros esperados (validação, não encontrado) |
| Problema | Quem chama pode esquecer de tratar | Forçado a tratar — TS não compila sem checagem |
1. Result Types — O padrão mais limpo
Conceito
Um Result type é um discriminated union que representa sucesso ou falha como valores, não exceções.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };A função que retorna Result força quem chama a tratar ambos os casos.
Frontend — Validação de formulário
// contracts/validation.ts
type ValidationError = { field: string; message: string };
type FormResult<T> = Result<T, ValidationError[]>;
// validators/checkout.validator.ts
function validateCheckout(data: unknown): FormResult<CheckoutData> {
const errors: ValidationError[] = [];
if (!data || typeof data !== 'object') {
return { ok: false, error: [{ field: 'form', message: 'Dados inválidos' }] };
}
if (!('email' in data) || typeof data.email !== 'string' || !data.email.includes('@')) {
errors.push({ field: 'email', message: 'Email inválido' });
}
if (!('cardNumber' in data) || typeof data.cardNumber !== 'string' || data.cardNumber.length !== 16) {
errors.push({ field: 'cardNumber', message: 'Número do cartão deve ter 16 dígitos' });
}
if (errors.length > 0) {
return { ok: false, error: errors };
}
return { ok: true, value: data as CheckoutData };
}
// Componente — TS FORÇA você a tratar erro e sucesso
function CheckoutForm() {
const handleSubmit = (formData: FormData) => {
const result = validateCheckout(Object.fromEntries(formData));
if (!result.ok) {
// ✅ TS sabe que result.error é ValidationError[]
result.error.forEach(err => {
showFieldError(err.field, err.message);
});
return;
}
// ✅ TS sabe que result.value é CheckoutData
submitOrder(result.value);
};
}Backend — Operações de banco
// repositories/user.repository.ts
type RepositoryError =
| { kind: 'NOT_FOUND'; id: string }
| { kind: 'DUPLICATE'; field: string; value: string }
| { kind: 'CONNECTION_ERROR'; message: string };
class UserRepository {
async findById(id: string): Promise<Result<User, RepositoryError>> {
try {
const user = await this.db.users.findOne({ _id: id });
if (!user) {
return { ok: false, error: { kind: 'NOT_FOUND', id } };
}
return { ok: true, value: user };
} catch (err) {
return { ok: false, error: { kind: 'CONNECTION_ERROR', message: String(err) } };
}
}
async create(data: CreateUserDto): Promise<Result<User, RepositoryError>> {
try {
const existing = await this.db.users.findOne({ email: data.email });
if (existing) {
return { ok: false, error: { kind: 'DUPLICATE', field: 'email', value: data.email } };
}
const user = await this.db.users.create(data);
return { ok: true, value: user };
} catch (err) {
return { ok: false, error: { kind: 'CONNECTION_ERROR', message: String(err) } };
}
}
}
// Service — TS FORÇA tratar cada tipo de erro
class UserService {
async register(data: CreateUserDto) {
const result = await this.userRepo.create(data);
if (!result.ok) {
// ✅ TS sabe que error é RepositoryError
switch (result.error.kind) {
case 'DUPLICATE':
return { status: 409, body: { error: `${result.error.field} já cadastrado` } };
case 'CONNECTION_ERROR':
return { status: 503, body: { error: 'Serviço indisponível' } };
}
}
// ✅ TS sabe que result.value é User
await this.emailService.sendWelcome(result.value.email);
return { status: 201, body: result.value };
}
}2. Custom Error Classes — Para erros inesperados
Estrutura base
// errors/base.error.ts
abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly isOperational: boolean; // true = esperado, false = bug
constructor(
message: string,
public readonly context?: Record<string, unknown>,
) {
super(message);
this.name = this.constructor.name;
}
}Frontend — Erros de API tipados
// errors/api.errors.ts
class ApiError extends AppError {
readonly isOperational = true;
constructor(
message: string,
public readonly statusCode: number,
public readonly errorCode: string,
context?: Record<string, unknown>,
) {
super(message, context);
}
}
class NetworkError extends ApiError {
constructor(message = 'Falha na conexão') {
super(message, 0, 'NETWORK_ERROR');
}
}
class UnauthorizedError extends ApiError {
constructor(message = 'Sessão expirada') {
super(message, 401, 'UNAUTHORIZED');
}
}
class ValidationError extends AppError {
readonly statusCode = 400;
readonly isOperational = true;
constructor(
public readonly fieldErrors: Record<string, string[]>,
) {
super('Erro de validação', { fieldErrors });
}
}
// Uso no frontend — handler global de erros
function handleApiError(error: unknown) {
if (error instanceof NetworkError) {
showToast('Sem conexão. Verifique sua internet.');
return;
}
if (error instanceof UnauthorizedError) {
redirectToLogin();
return;
}
if (error instanceof ValidationError) {
// ✅ TS sabe que fieldErrors existe
Object.entries(error.fieldErrors).forEach(([field, messages]) => {
showFieldError(field, messages.join(', '));
});
return;
}
// Erro desconhecido
showToast('Algo deu errado. Tente novamente.');
reportError(error); // envia para Sentry, Datadog, etc.
}Backend — Middleware de erro no Express
// errors/order.errors.ts
class InsufficientStockError extends AppError {
readonly statusCode = 409;
readonly isOperational = true;
constructor(
public readonly productId: string,
public readonly requested: number,
public readonly available: number,
) {
super(`Estoque insuficiente para ${productId}`, { productId, requested, available });
}
}
class PaymentDeclinedError extends AppError {
readonly statusCode = 402;
readonly isOperational = true;
constructor(
public readonly reason: string,
public readonly gatewayCode: string,
) {
super(`Pagamento recusado: ${reason}`, { gatewayCode });
}
}
// middleware/error-handler.ts — Express error middleware
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
if (err instanceof AppError) {
// Erro operacional — esperado, log como warning
logger.warn(err.message, { statusCode: err.statusCode, context: err.context });
return res.status(err.statusCode).json({
error: err.message,
code: err.constructor.name,
...(err instanceof ValidationError && { fields: err.fieldErrors }),
...(err instanceof InsufficientStockError && {
productId: err.productId,
available: err.available,
}),
});
}
// Erro não operacional — bug, log como error
logger.error('Unhandled error', { error: err.stack });
return res.status(500).json({ error: 'Erro interno do servidor' });
}3. Narrowing de erros no catch
O TypeScript tipa catch como unknown (com useUnknownInCatchVariables ou strict). Isso é uma feature, não um bug — te força a validar antes de acessar propriedades.
// ❌ Assumir que o erro é Error
try {
await fetch('/api/data');
} catch (err) {
console.log(err.message); // ❌ TS erro — err pode ser string, number, null, anything
}
// ✅ Validar antes de usar
try {
await fetch('/api/data');
} catch (err) {
if (err instanceof Error) {
console.log(err.message); // ✅ TS sabe que é Error
} else {
console.log('Erro desconhecido:', err);
}
}
// ✅✅ Com type guard reutilizável
function isError(value: unknown): value is Error {
return value instanceof Error;
}
try {
await riskyOperation();
} catch (err) {
const message = isError(err) ? err.message : String(err);
logger.error(message);
}4. Funções utilitárias para Result
// utils/result.ts — helpers que simplificam o uso
// Extrai o valor ou lança o erro
function unwrap<T, E>(result: Result<T, E>): T {
if (result.ok) return result.value;
throw result.error;
}
// Mapeia o valor de sucesso
function mapResult<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
if (result.ok) return { ok: true, value: fn(result.value) };
return result;
}
// Combina múltiplos Results
function combineResults<T, E>(results: Result<T, E>[]): Result<T[], E> {
const values: T[] = [];
for (const result of results) {
if (!result.ok) return result;
values.push(result.value);
}
return { ok: true, value: values };
}
// Uso:
const results = await Promise.all([
validateEmail(data.email),
validatePassword(data.password),
validateCPF(data.cpf),
]);
const combined = combineResults(results);
if (!combined.ok) {
return res.status(400).json({ error: combined.error });
}
// combined.value é [string, string, string]Resumo — Quando usar cada abordagem
| Situação | Abordagem | Exemplo |
|---|---|---|
| Validação de input | Result type | validateEmail() retorna Result<string, Error> |
| Operação de banco esperada | Result type | findById() retorna Result<User | null, Error> |
| Erro de rede/conexão | Custom Error + try/catch | NetworkError lançado em catch |
| Erro de regra de negócio | Custom Error | InsufficientStockError |
| Erro global não esperado | Error middleware | Express error handler |
| Erro em componente React | Result type ou Error Boundary | ApiState discriminated union |
Referências
Discriminated Unions em TypeScript
O padrão mais poderoso de TypeScript para modelar estados, variantes e resultados — exemplos full-stack com React, NestJS e Express.
Design Patterns em TypeScript
Padrões de projeto clássicos adaptados para TypeScript full-stack — creacionais, estruturais e comportamentais com exemplos reais.