Padrões de Error Handling para Sistemas Distribuídos em Produção
Como estruturar tratamento de erros em arquiteturas de microsserviços para transformar falhas inevitáveis em resiliência operacional.
Resumo executivo
Como estruturar tratamento de erros em arquiteturas de microsserviços para transformar falhas inevitáveis em resiliência operacional.
Ultima atualizacao: 17/03/2026
A inevitabilidade da falha em sistemas distribuídos
Em arquiteturas monolíticas, quando uma coisa quebra, geralmente é uma coisa específica e você pode debugar localmente. Em sistemas distribuídos com dezenas de microsserviços, bancos de dados, filas e caches, coisas quebram o tempo todo.
Falhas de rede. Servidores que param. Bases de dados que ficam sobrecarregadas. APIs de terceiros que retornam 500. Deploys simultâneos em múltiplos serviços. Problemas de DNS. Certificados expirados.
A questão não é se seu sistema vai falhar, mas como ele falha. Um sistema que falha bem — de forma previsível, observável e recuperável — é muito mais valioso que um sistema que "nunca falha" até falhar catastrófico.
Error handling estruturado é o que separa sistemas frágeis de sistemas resilientes.
O espectro de falhas que você precisa tratar
Falhas temporárias (transient failures)
- Timeout de rede intermitente
- Database connection pool momentaneamente exausto
- API gateway retornando 503 brevemente
Padrão: Retry com backoff exponencial.
Falhas permanentes (permanent failures)
- Serviço destruído
- API endpoint removido
- Database schema incompatível
Padrão: Fallback imediato + alerta.
Falhas parciais (partial failures)
- Um de três réplicas de database cai
- Uma zona de disponibilidade (AZ) degrada
- Cache parcialmente disponível
Padrão: Circuit breaker + degradação graciosa.
Falhas em cascata (cascading failures)
- Um serviço sobrecarrega downstream
- Retry storm derruba banco de dados
- Circuit breaker quebrado em múltiplas dependências
Padrão: Bulkhead + timeout hierárquico.
Padrão 1: Retry com backoff inteligente
Retries ingênuos transformam problemas pequenos em desastres. Retry storms — quando todos os clientes retentam simultaneamente — podem derrubar sistemas que normalmente funcionam bem.
Anti-padrão: Retry linear agressivo
typescript// RUIM: Retry fixo sem jitter
async function fetchWithRetry(url: string, maxRetries: number = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url);
} catch (error) {
if (i === maxRetries - 1) throw error;
await delay(100); // 100ms fixo
}
}
}Problema: 5 clientes × 5 retries = 25 requisições simultâneas para cada falha original. Isso esmaga recursos downstream.
Padrão: Backoff exponencial com jitter
typescriptinterface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
retryableErrors: (error: Error) => boolean;
}
async function fetchWithExponentialBackoff<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let attempt = 0;
while (attempt < config.maxAttempts) {
try {
return await fn();
} catch (error) {
attempt++;
if (attempt >= config.maxAttempts || !config.retryableErrors(error as Error)) {
throw error;
}
// Backoff exponencial com jitter
const baseDelay = Math.min(
config.baseDelayMs * Math.pow(2, attempt),
config.maxDelayMs
);
const jitter = baseDelay * (0.5 + Math.random() * 0.5);
const delayMs = Math.floor(baseDelay + jitter);
metrics.increment('retry.attempt', {
error_type: error.constructor.name,
attempt: attempt.toString()
});
await delay(delayMs);
}
}
throw new Error('Max retries exceeded');
}
// Uso
const result = await fetchWithExponentialBackoff(
() => fetch('https://api.example.com/data').then(r => r.json()),
{
maxAttempts: 4,
baseDelayMs: 100, // 100ms, 200ms, 400ms, 800ms
maxDelayMs: 5000,
retryableErrors: (error) =>
error instanceof TypeError || // Network error
(error as any)?.status >= 500 // Server error
}
);Benefícios:
- Jitter evita thundering herd
- Backoff exponencial dá espaço de recuperação ao downstream
- Retry condicionado evita retry em erros que não vão sumir
Padrão 2: Circuit breaker
Circuit breaker previne que operações que estão falhando sejam chamadas repetidamente, permitindo que o sistema se recupere e economizando recursos.
Implementação de circuit breaker
typescriptenum CircuitState {
CLOSED = 'CLOSED', // Operação normal
OPEN = 'OPEN', // Bloqueia chamadas
HALF_OPEN = 'HALF_OPEN' // Testa recuperação
}
interface CircuitBreakerConfig {
failureThreshold: number; // Falhas antes de abrir
successThreshold: number; // Sucessos para fechar (half-open)
timeoutMs: number; // Tempo antes de tentar half-open
windowMs: number; // Janela de contagem de falhas
}
class CircuitBreaker<T> {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private lastFailureTime = 0;
private failures: number[] = [];
constructor(
private fn: () => Promise<T>,
private config: CircuitBreakerConfig
) {
// Limpa contagem de falhas antigas
setInterval(() => {
const now = Date.now();
this.failures = this.failures.filter(f => now - f < this.config.windowMs);
this.failureCount = this.failures.length;
}, this.config.windowMs / 2);
}
async execute(): Promise<T> {
// Se circuito está aberto e timeout passou, tenta half-open
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.config.timeoutMs) {
this.state = CircuitState.HALF_OPEN;
metrics.increment('circuit.half_open');
} else {
throw new CircuitBreakerOpenError('Circuit is OPEN');
}
}
try {
const result = await this.fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.config.successThreshold) {
this.state = CircuitState.CLOSED;
this.successCount = 0;
metrics.increment('circuit.closed');
}
} else {
this.failures = this.failures.filter(f => Date.now() - f < this.config.windowMs);
}
}
private onFailure() {
this.failureCount++;
this.failures.push(Date.now());
this.lastFailureTime = Date.now();
if (this.failureCount >= this.config.failureThreshold) {
this.state = CircuitState.OPEN;
this.successCount = 0;
metrics.increment('circuit.open');
}
}
getState(): CircuitState {
return this.state;
}
}
class CircuitBreakerOpenError extends Error {
constructor(message: string) {
super(message);
this.name = 'CircuitBreakerOpenError';
}
}
// Uso
const paymentCircuitBreaker = new CircuitBreaker(
() => fetch('https://payments.api/charge').then(r => r.json()),
{
failureThreshold: 5, // Abre após 5 falhas
successThreshold: 2, // Fecha após 2 sucessos (half-open)
timeoutMs: 60000, // 1 minuto antes de tentar recuperação
windowMs: 30000 // Janela de 30s para contagem de falhas
}
);
try {
const result = await paymentCircuitBreaker.execute();
} catch (error) {
if (error instanceof CircuitBreakerOpenError) {
// Usa fallback
return await fallbackPaymentFlow();
}
throw error;
}Padrão 3: Fallback e graceful degradation
Nem todo precisa funcionar perfeitamente. Sistemas resilientes degradam graciotivamente — features não-críticas falham silenciosamente enquanto funcionalidade principal permanece disponível.
Degradação por criticidade
typescriptinterface ServiceConfig {
name: string;
critical: boolean; // Se é core ou nice-to-have
fallback?: () => Promise<any>;
timeoutMs: number;
}
class GracefulDegradation {
private services: Map<string, ServiceConfig> = new Map();
register(config: ServiceConfig) {
this.services.set(config.name, config);
}
async execute<T>(serviceName: string, fn: () => Promise<T>): Promise<T | null> {
const config = this.services.get(serviceName);
if (!config) throw new Error(`Unknown service: ${serviceName}`);
try {
return await Promise.race([
fn(),
timeout(config.timeoutMs)
]);
} catch (error) {
metrics.increment('service.error', {
service: serviceName,
critical: config.critical.toString()
});
if (config.critical) {
// Serviços críticos: alerta imediata
alerting.sendCritical(
`Critical service ${serviceName} failed`,
{ error }
);
throw error;
}
// Serviços não-críticos: fallback silencioso
if (config.fallback) {
try {
const fallbackResult = await config.fallback();
metrics.increment('service.fallback_success', { service: serviceName });
return fallbackResult;
} catch (fallbackError) {
metrics.increment('service.fallback_failed', { service: serviceName });
return null; // Falha graciosa completa
}
}
return null;
}
}
}
// Configuração
const degradation = new GracefulDegradation();
degradation.register({
name: 'payments',
critical: true,
timeoutMs: 5000
});
degradation.register({
name: 'recommendations',
critical: false,
timeoutMs: 1000,
fallback: () => Promise.resolve([]) // Retorna lista vazia
});
degradation.register({
name: 'analytics',
critical: false,
timeoutMs: 2000
// Sem fallback = falha silenciosa completa
});
// Uso
async function handleUserRequest(userId: string) {
try {
const payment = await degradation.execute('payments', () => createPayment(userId));
// Pagamento é crítico: erro aqui dispara alerta
} catch (error) {
// Handle payment error
}
const recommendations = await degradation.execute(
'recommendations',
() => fetchRecommendations(userId)
);
// Se falhar, recommendations = null, mas request não falha
// Analytics: nem tenta, se falhou antes retorna null
degradation.execute('analytics', () => trackEvent('user_view'));
}Padrão 4: Timeout hierárquico
Timeouts são sua última linha de defesa. Sem timeouts, uma requisição travada pode travar threads indefinidamente, esgotando recursos.
Hierarquia de timeouts
Timeout de Cliente (3s) < Timeout de Gateway (5s) < Timeout de Serviço (10s)Cada layer precisa de timeout menor que o anterior. Se cliente timeout antes do serviço, serviço ainda pode responder e processar a operação, mas cliente já desconectou — desperdício de recursos.
typescriptinterface TimeoutConfig {
client: number; // Timeout no cliente (mínimo)
gateway: number; // Timeout no gateway
service: number; // Timeout no serviço (máximo)
}
function validateTimeouts(config: TimeoutConfig) {
if (config.client >= config.gateway) {
throw new Error('Client timeout must be less than gateway timeout');
}
if (config.gateway >= config.service) {
throw new Error('Gateway timeout must be less than service timeout');
}
}
// Configuração consistente
const TIMEOUTS: Record<string, TimeoutConfig> = {
payments: { client: 3000, gateway: 5000, service: 10000 },
analytics: { client: 500, gateway: 1000, service: 2000 },
recommendations: { client: 1000, gateway: 2000, service: 5000 }
};
async function fetchWithTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number
): Promise<T> {
return Promise.race([
fn(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new TimeoutError()), timeoutMs)
)
]);
}
class TimeoutError extends Error {
constructor() {
super('Operation timed out');
this.name = 'TimeoutError';
}
}
// Uso consistente
async function callPaymentsAPI() {
const config = TIMEOUTS.payments;
return await fetchWithTimeout(
() => fetch('https://payments.api/charge').then(r => r.json()),
config.client // Usa timeout do cliente
);
}Padrão 5: Bulkhead para isolamento de falha
Bulkhead separa recursos por tipo de operação, impedindo que falha em um tipo afete outros.
typescriptclass Bulkhead<T> {
private queue: Array<{ fn: () => Promise<T>; resolve: (value: T) => void; reject: (error: Error) => void }> = [];
private running = 0;
constructor(
private maxConcurrent: number,
private queueLimit: number = 100
) {}
async execute(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
if (this.queue.length >= this.queueLimit) {
reject(new Error('Bulkhead queue full'));
metrics.increment('bulkhead.rejected');
return;
}
this.queue.push({ fn, resolve, reject });
this.processQueue();
});
}
private async processQueue() {
while (this.running < this.maxConcurrent && this.queue.length > 0) {
const task = this.queue.shift();
if (!task) break;
this.running++;
metrics.gauge('bulkhead.running', this.running);
try {
const result = await task.fn();
task.resolve(result);
} catch (error) {
task.reject(error as Error);
} finally {
this.running--;
metrics.gauge('bulkhead.running', this.running);
this.processQueue(); // Próxima tarefa
}
}
}
}
// Bulkheads separados por tipo de operação
const bulkheads = {
read: new Bulkhead(50, 100), // Até 50 operações de leitura simultâneas
write: new Bulkhead(10, 50), // Até 10 operações de escrita simultâneas
analytics: new Bulkhead(5, 20) // Até 5 requisições de analytics simultâneas
};
// Uso
async function readUserData(userId: string) {
return await bulkheads.read.execute(() =>
db.users.findById(userId)
);
}
async function writeUserData(userId: string, data: any) {
return await bulkheads.write.execute(() =>
db.users.update(userId, data)
);
}
// Analytics não derruba escrita se falhar
async function trackAnalytics() {
try {
return await bulkheads.analytics.execute(() =>
analyticsApi.track('event')
);
} catch (error) {
// Analytics falhou, mas operação principal continua
metrics.increment('analytics.dropped');
}
}Observabilidade de errors
Sem observabilidade, você está cego. Error handling sem logging e métricas é apenas esconder problemas.
Logging estruturado de errors
typescriptinterface ErrorContext {
service: string;
operation: string;
userId?: string;
requestId?: string;
upstream?: string;
metadata?: Record<string, any>;
}
function logError(error: Error, context: ErrorContext) {
logger.error({
event: 'error_occurred',
error_type: error.constructor.name,
error_message: error.message,
error_stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
...context,
timestamp: new Date().toISOString()
});
}
// Uso
try {
await someOperation();
} catch (error) {
logError(error as Error, {
service: 'payment-service',
operation: 'process_payment',
userId: request.userId,
requestId: request.id,
upstream: 'https://payments.api/charge',
metadata: { amount: 10000, currency: 'BRL' }
});
throw error;
}Métricas de errors
typescriptinterface ErrorMetrics {
total: number;
byType: Map<string, number>;
byOperation: Map<string, number>;
byUpstream: Map<string, number>;
}
class ErrorTracker {
private metrics: ErrorMetrics = {
total: 0,
byType: new Map(),
byOperation: new Map(),
byUpstream: new Map()
};
track(error: Error, context: ErrorContext) {
this.metrics.total++;
this.incrementMap(this.metrics.byType, error.constructor.name);
this.incrementMap(this.metrics.byOperation, context.operation);
if (context.upstream) {
this.incrementMap(this.metrics.byUpstream, context.upstream);
}
// Exporta para sistema de métricas
this.exportToMetricsSystem();
}
private incrementMap(map: Map<string, number>, key: string) {
map.set(key, (map.get(key) || 0) + 1);
}
private exportToMetricsSystem() {
// Exporta para Prometheus/DataDog/etc
for (const [type, count] of this.metrics.byType) {
metrics.gauge('errors.total', count, { error_type: type });
}
}
getErrorRate(): number {
const totalRequests = metrics.get('requests.total');
return this.metrics.total / totalRequests;
}
}Conclusão
Error handling em sistemas distribuídos não é uma lista de padrões para implementar uma vez e esquecer. É uma disciplina contínua que evolui com seu sistema.
Comece com o que tem maior impacto: retry com backoff, timeouts e circuit breaker. Adicione graceful degradation quando você entender quais serviços são críticos e quais são nice-to-have. Implemente bulkheads quando você tiver problemas de isolamento de falha. Sempre acompanhe com observabilidade — logs estruturados e métricas de errors são seus olhos e ouvidos.
O objetivo não é eliminar erros — isso é impossível em sistemas distribuídos. O objetivo é fazer com que erros sejam previsíveis, observáveis e recuperáveis. Quando um erro acontece, você quer saber o que aconteceu, por que aconteceu, e qual foi o impacto. E você quer que o sistema continue operando, mesmo que em modo degradado.
Sistemas que tratam erros bem podem falhar o tempo todo e ainda parecer confiáveis para usuários. Sistemas que tratam erros mal podem funcionar 99.9% do tempo e parecer quebrados quando uma coisa falha catastrófica.
Sua arquitetura de microsserviços precisa de error handling estruturado? Fale com especialistas da Imperialis em sistemas resilientes para projetar padrões de error handling que transformam falhas em resiliência operacional.