Práticas recomendadas para tratamento de erros

Tratamento de erros de software: como classificar, registrar e recuperar erros em sistemas de produção.

O tratamento de erros não é um recurso que se adiciona depois que o sistema já está funcionando. É uma decisão de projeto que determina como um sistema se comporta quando as coisas param de funcionar, o que, em produção, é uma questão de quando, não de se. Redes podem sofrer interrupções. Bancos de dados podem ficar temporariamente indisponíveis. Usuários podem enviar dados que violam todas as premissas do desenvolvedor. Serviços externos podem retornar respostas inesperadas. Hardware pode falhar. O sistema que lida com todas essas condições de forma previsível, sem corromper dados ou expor informações sensíveis, é bem projetado. O sistema que trava, corrompe silenciosamente o estado ou vaza detalhes internos de implementação quando qualquer uma dessas situações ocorre tem um problema estrutural que nenhuma quantidade de desenvolvimento de recursos resolverá.

Tratamento de erros para toda a sua base de código.

SMART TS XL Detecta exceções não tratadas e lacunas no tratamento de erros em todas as linguagens e plataformas do seu ambiente.

Explorar SMART TS XL

As consequências práticas do tratamento inadequado de erros não são hipotéticas. O tratamento inadequado de erros é agora explicitamente reconhecido como um dos riscos de segurança mais críticos no desenvolvimento de software: OWASP A10:2025 (Tratamento Inadequado de Condições Excepcionais concentra-se no tratamento inadequado de erros, erros lógicos, falhas de abertura e outros cenários relacionados decorrentes de condições anormais) que os sistemas encontram. Esta é uma nova categoria no OWASP Top 10 de 2025, refletindo uma compreensão mais madura de como as falhas no tratamento de erros produzem não apenas instabilidade operacional, mas também vulnerabilidades de segurança exploráveis. Fraquezas notáveis ​​nesta categoria incluem CWE-209 Geração de Mensagem de Erro Contendo Informações Sensíveis, CWE-476 Desreferência de Ponteiro Nulo e CWE-636 Falha Não Segura. Cada uma delas é evitável com práticas disciplinadas de tratamento de erros aplicadas de forma consistente em toda a base de código.

O que é tratamento de erros no desenvolvimento de software?

O tratamento de erros é o conjunto de mecanismos pelos quais um sistema de software detecta, classifica e responde a condições que impedem a execução normal. Inclui a captura de exceções, o gerenciamento do estado de erro, o registro de diagnósticos, a comunicação de falhas aos usuários ou sistemas subsequentes e a recuperação ou o encerramento controlado do processo afetado. Um sistema com tratamento de erros adequado não é um sistema que nunca falha: é um sistema que responde a falhas de forma previsível, sem corrupção de dados, sem expor informações sensíveis e sem propagar a falha para componentes que poderiam continuar operando.

Essa distinção entre falhar de forma previsível e falhar de forma caótica é operacionalmente significativa. Um sistema que falha de forma previsível gera registros claros, aciona mecanismos de recuperação definidos e fornece à equipe de operações as informações necessárias para diagnosticar e resolver o problema. Um sistema que falha de forma caótica gera registros incompletos, permite que erros silenciosos corrompam o estado antes que qualquer falha visível venha à tona e força a equipe de plantão a gastar a maior parte do tempo de resposta ao incidente reconstruindo o que aconteceu em vez de resolvê-lo. A diferença entre um incidente de dez minutos e um incidente de três horas geralmente não está na falha em si, mas na qualidade do tratamento de erros que a envolve.

O tratamento de erros também tem implicações diretas na segurança. O problema de segurança mais comum causado pelo tratamento inadequado de erros ocorre quando mensagens de erro internas detalhadas, como rastreamentos de pilha, despejos de banco de dados e códigos de erro, são exibidas ao usuário. Essas mensagens revelam detalhes de implementação que nunca deveriam ser divulgados, fornecendo aos hackers pistas importantes sobre possíveis falhas no site. Um tratamento de erros eficaz mantém uma separação estrita entre as informações de diagnóstico registradas internamente e as informações retornadas aos usuários ou expostas por meio de APIs.

Tipos de erros de software e como identificá-los

Os erros de software não constituem uma categoria uniforme. Eles diferem em relação a quando ocorrem, como são detectados, qual resposta exigem e se essa resposta pode ser automatizada. Compreender a taxonomia é o pré-requisito para projetar uma estratégia de tratamento adequada a cada tipo de erro, em vez de aplicar o mesmo mecanismo a todos eles.

Erros de sintaxe

Erros de sintaxe ocorrem quando o código viola as regras gramaticais da linguagem de programação. Compiladores e interpretadores os detectam antes da execução, tornando-os a categoria mais fácil de lidar: eles não chegam à produção em sistemas com pipelines de construção automatizados. Em linguagens interpretadas como Python ou JavaScript, no entanto, erros de sintaxe em trechos de código não testados podem chegar à produção e causar falhas em tempo de execução quando esses trechos são executados pela primeira vez. Ferramentas de linting e análise estática detectam erros de sintaxe nesses ambientes antes da implantação.

Erros de tempo de execução

Erros de tempo de execução ocorrem durante a execução quando o programa encontra uma condição que não pode ser tratada pelo fluxo de controle normal: uma desreferência de ponteiro nulo, uma divisão por zero, um arquivo inexistente, uma falha na conexão de rede, um banco de dados temporariamente indisponível. Eles são o principal alvo dos mecanismos de tratamento de erros em sistemas de produção porque são imprevisíveis, dependem de condições externas fora do controle do código e podem ocorrer em qualquer ponto durante a execução de uma transação.

Os erros de tempo de execução se dividem ainda em condições recuperáveis ​​e irrecuperáveis, sendo esta a classificação mais importante que o sistema de tratamento de erros deve fazer em termos operacionais. Uma falha temporária na conexão com o banco de dados é um erro de tempo de execução recuperável: tentar novamente após um breve intervalo provavelmente terá sucesso. Um arquivo de configuração corrompido que impede a inicialização do aplicativo é um erro de tempo de execução irrecuperável: tentar novamente não ajudará, e a resposta correta é o encerramento controlado com uma mensagem de diagnóstico clara. Tratar essas duas categorias de forma idêntica, aplicando a mesma lógica de repetição a uma condição que a repetição não consegue resolver, é uma das fontes mais comuns de comportamento descontrolado no tratamento de erros em sistemas de produção.

Erros de lógica

Os erros de lógica são a categoria mais perigosa justamente porque são invisíveis aos mecanismos padrão de tratamento de erros. O programa executa sem lançar nenhuma exceção, mas produz resultados incorretos porque a lógica implementada não corresponde ao comportamento esperado. Um cálculo de preço com um erro de um em um loop, uma comparação de datas que não leva em conta as diferenças de fuso horário, uma verificação de autorização que concede acesso ao conjunto errado de usuários: esses são erros de lógica. Eles não acionam nenhum manipulador de exceção, não aparecem em nenhum log de erros e, frequentemente, propagam seus resultados incorretos por vários sistemas subsequentes antes que alguém perceba que algo está errado.

A detecção de erros lógicos exige a validação dos resultados, e não apenas a captura de exceções. Isso significa asserções que verificam as pós-condições, testes de comparação que validam as saídas em relação a uma referência correta conhecida e monitoramento que alerta quando as métricas de negócios se desviam dos intervalos esperados.

Erros do sistema

Os erros de sistema têm origem fora do código da aplicação: falhas de hardware, esgotamento de memória, limites de recursos do sistema operacional, falhas na infraestrutura de rede. Normalmente, não podem ser resolvidos apenas pela aplicação e exigem respostas coordenadas com a camada de infraestrutura: failover para componentes redundantes, degradação gradual para funcionalidade reduzida ou desligamento controlado com notificação à equipe de operações. O papel do código da aplicação é detectar essas condições precocemente, responder com degradação apropriada em vez de falha catastrófica e produzir informações de diagnóstico que permitam à equipe de infraestrutura entender o que ocorreu.

A tabela abaixo relaciona cada tipo de erro ao seu mecanismo de detecção e à estratégia de resposta apropriada:

Tipo de ErroQuando isso ocorreMecanismo de DetecçãoEstratégia de Resposta
SintaxeTempo de compilação/interpretaçãoCompilador, linter, análise estáticaCorrigir antes da implementação
Tempo de execução (recuperável)ExecuçãoTry-catch, tratamento de exceçõesTentar novamente com recuo, caminho alternativo
Tempo de execução (irrecuperável)ExecuçãoTry-catch, tratamento de exceçõesRescisão controlada, escalonamento
LógicaExecuçãoValidação de resultados, monitoramentoCorreção lógica, auditoria de dados
SystemExecuçãoMonitoramento e alertas de infraestruturaFailover, degradação gradual

Consequências do tratamento inadequado de erros

As consequências de um tratamento inadequado de erros se dividem em quatro categorias, cada uma com impacto direto nas operações ou nos negócios. Compreendê-las concretamente é o que justifica o investimento em engenharia numa abordagem sistemática de tratamento de erros.

Instabilidade de aplicativos e falhas em cascata

Uma exceção não tratada que se propaga até o topo da pilha de chamadas encerra o processo ou thread que a encontrou. Em uma aplicação web, isso significa que a requisição do usuário não recebe resposta, ou recebe uma resposta de erro genérica que não fornece informações úteis. Em sistemas com transações ativas ou estado de sessão, a transação pode ficar em um estado parcialmente concluído, o que é inconsistente do ponto de vista do banco de dados.

Em arquiteturas de microsserviços, a instabilidade da aplicação causada por erros não tratados tem um efeito multiplicativo. Um serviço que não implementa disjuntores em suas dependências externas, quando essas dependências se tornam lentas ou indisponíveis, esgotará seu próprio pool de conexões tentando requisições que não são concluídas. Uma vez que o pool de conexões esteja esgotado, o serviço fica indisponível para seus próprios chamadores upstream, independentemente de a causa raiz envolver esses chamadores. O tratamento inadequado de erros, como ignorar exceções, vazar dados sensíveis em mensagens de erro ou falhar silenciosamente, é uma fonte comum tanto de bugs quanto de vulnerabilidades de segurança. Falhar silenciosamente é particularmente prejudicial em sistemas distribuídos, pois permite que a falha se propague invisivelmente antes que qualquer alerta seja disparado.

Corrupção da Integridade de Dados

Erros que ocorrem no meio de operações de escrita com várias etapas podem deixar o sistema em um estado inconsistente se essas operações não forem encapsuladas em transações atômicas. O exemplo clássico é o processamento de pagamentos: se a cobrança no método de pagamento do usuário for bem-sucedida, mas a criação do registro do pedido correspondente falhar sem acionar uma transação de compensação, o usuário terá sido cobrado por uma compra que não existe no sistema. Resolver isso posteriormente exige uma conciliação manual, que é cara, propensa a erros e incompleta.

Falhas na integridade dos dados causadas por tratamento inadequado de erros são frequentemente descobertas muito tempo depois, quando os sistemas subsequentes que consumiram os dados incorretos já tomaram medidas com base neles. O custo da correção aumenta com o atraso entre o erro e sua descoberta, razão pela qual a prevenção por meio do design de transações atômicas é significativamente mais barata do que a correção.

Vulnerabilidades de segurança a partir da saída de erros

A exposição de dados sensíveis devido ao tratamento inadequado de erros de banco de dados, que revela o erro completo do sistema ao usuário, fornece aos atacantes as informações necessárias para criar ataques mais direcionados. Isso agora é formalmente classificado como um dos dez principais riscos de segurança no OWASP 2025. Os rastreamentos de pilha expostos em respostas HTTP revelam versões de frameworks, caminhos de arquivos, nomes de classes e assinaturas de métodos. As mensagens de erro do banco de dados revelam nomes de tabelas, nomes de colunas e estruturas de consultas. Esses detalhes reduzem o esforço necessário para criar um ataque bem-sucedido de injeção de SQL ou de travessia de diretório, transformando-o de mera adivinhação em um ataque direcionado e preciso.

A correção requer duas coisas: primeiro, que todos os manipuladores de exceção no limite voltado para o usuário retornem apenas mensagens apropriadas para o usuário, nunca detalhes internos; e segundo, que as informações de diagnóstico internas sejam capturadas em um sistema de registro com controles de acesso apropriados, em vez de serem descartadas. A mensagem para o usuário e a mensagem de diagnóstico têm propósitos diferentes e devem ser geradas independentemente.

Dívida de manutenção resultante de tratamento inconsistente de erros

Bases de código sem uma abordagem padronizada para tratamento de erros acumulam dívida técnica de manutenção à medida que crescem. Cada desenvolvedor implementa suas próprias convenções: alguns usam exceções personalizadas, alguns retornam códigos de erro, alguns registram o erro no momento da ocorrência, alguns o propagam sem registro. O resultado é um sistema onde reconstruir a causa de uma falha em produção exige a leitura de múltiplos arquivos de log com formatos incompatíveis, a compreensão de convenções de tratamento de erros que diferem por módulo e por quem o escreveu, e a descoberta frequente de que a causa raiz real não foi registrada porque o bloco `catch` relevante estava vazio ou registrou apenas uma mensagem genérica que descartou o contexto da exceção original.

Melhores práticas de tratamento de erros em engenharia de software

As boas práticas a seguir não são preferências estilísticas. Cada uma aborda um modo de falha específico que gera incidentes de produção quando a prática está ausente. Elas estão ordenadas da mais básica para a mais avançada, refletindo a ordem em que uma equipe que esteja construindo ou adaptando um sistema de tratamento de erros deve abordá-las.

Classificar erros como recuperáveis ​​ou irrecuperáveis ​​no momento da detecção.

Toda decisão de tratamento de erros começa com uma simples classificação: este erro pode ser resolvido sem intervenção humana ou requer escalonamento ou encerramento do processo? Essa classificação deve ocorrer no momento em que o erro é detectado pela primeira vez, e não ser adiada para um nível superior da pilha de chamadas, onde o contexto que a fundamenta já se perdeu.

Erros recuperáveis ​​são aqueles em que uma nova tentativa, o recurso a um caminho alternativo ou uma resposta com funcionalidade reduzida podem concluir a operação de forma aceitável. Erros irrecuperáveis ​​são aqueles em que a continuação da execução produziria resultados incorretos, corromperia dados ou criaria uma vulnerabilidade de segurança. A ausência de um arquivo de configuração necessário, a detecção de corrupção de dados em um armazenamento crítico e o esgotamento de um recurso sem alternativa são exemplos de erros irrecuperáveis. Um tempo limite de rede transitório, uma resposta de limite de taxa de uma API externa e um serviço secundário temporariamente indisponível são exemplos de erros recuperáveis.

Classificar erroneamente um erro irrecuperável como recuperável e aplicar lógica de repetição a ele gera tempestades de repetição: um processo que entra em loop indefinidamente contra uma condição que nenhuma nova tentativa consegue melhorar, consumindo recursos que poderiam ser usados ​​para atender outras solicitações. Classificar erroneamente um erro recuperável como irrecuperável e encerrar o processo gera tempo de inatividade desnecessário. A classificação é uma decisão de projeto que deve ser documentada para cada tipo de erro, e não tomada de forma ad hoc em cada bloco catch.

Implementar tratamento centralizado de erros

O tratamento centralizado de erros significa que um único local no sistema é responsável por receber os erros, classificá-los, registrá-los com metadados padronizados e determinar a política de resposta. Os módulos individuais detectam e propagam os erros, mas não são responsáveis ​​pelo formato de registro, pelo limite de alerta ou pela estratégia de resposta. Esses parâmetros são definidos uma única vez no manipulador centralizado e aplicados de forma consistente.

Em uma aplicação web, o tratamento centralizado de erros geralmente assume a forma de um componente middleware que captura todas as exceções não tratadas no limite da requisição, registra-as com o contexto da requisição (identificador do usuário, identificador da requisição, endpoint, duração), aplica a lógica de classificação e retorna uma resposta apropriada à classe do erro. Frameworks de linguagem fornecem a interface para isso: middleware do Express no Node.js, @ControllerAdvice Na primavera, componentes de limite de erro no React, app.errorhandler Em Flask.

A vantagem é a consistência. Todos os erros registrados em qualquer lugar do sistema têm o mesmo formato. Todos os erros que ultrapassam a interface visível ao usuário são filtrados pela mesma lógica de higienização. Todos os erros que ultrapassam um limite de gravidade definido acionam o mesmo alerta. Essa consistência é o que torna a análise de logs e a resposta a incidentes eficientes, em vez de artesanais.

Implemente o recuo exponencial com jitter para novas tentativas.

As tentativas de reconexão sem recuo amplificam o problema que tentam resolver. Se um banco de dados estiver temporariamente sobrecarregado e cem clientes começarem simultaneamente a tentar novamente solicitações com falha em intervalos de um segundo, o tráfego de reconexão pode impedir completamente a recuperação do banco de dados. O recuo exponencial aumenta o atraso entre as tentativas progressivamente, reduzindo a pressão de reconexão sobre o componente com falha e dando-lhe tempo para se recuperar.

A variação de atraso (jitter) introduz aleatoriedade no intervalo de tempo para evitar avalanches de novas tentativas: se todos os clientes usarem o mesmo cronograma de espera determinístico, todos tentarão novamente no mesmo instante após cada período de atraso, reproduzindo o problema de sincronização. Aleatorizar o atraso dentro de um intervalo garante que o tráfego de novas tentativas de vários clientes seja distribuído ao longo do tempo, em vez de sincronizado.

As tentativas de repetição só são seguras quando a operação que está sendo repetida é idempotente, ou seja, quando executá-la várias vezes produz o mesmo resultado que executá-la uma única vez. As operações de leitura são inerentemente idempotentes. As operações de escrita devem ser idempotentes por projeto, normalmente pela inclusão de uma chave de idempotência na solicitação que o servidor usa para evitar entregas duplicadas da mesma solicitação.

python

import time
import random

def with_retry(operation, max_attempts=4, base_delay_seconds=1.0):
    """
    Execute an operation with exponential backoff and jitter.
    Only retries on recoverable IOError and TimeoutError.
    Propagates all other exceptions immediately without retry.
    """
    for attempt in range(max_attempts):
        try:
            return operation()
        except (IOError, TimeoutError) as exc:
            if attempt == max_attempts - 1:
                raise  # exhausted retries, propagate
            delay = base_delay_seconds * (2 ** attempt) + random.uniform(0, 0.5)
            print(f"Attempt {attempt + 1} failed ({exc}). Retrying in {delay:.1f}s")
            time.sleep(delay)
        except Exception:
            raise  # unrecoverable, do not retry

Utilize o registro estruturado com contexto de diagnóstico completo.

Uma entrada de log que contém apenas a mensagem de exceção, sem contexto sobre qual operação estava sendo executada, quais entradas foram recebidas e em que estado o sistema se encontrava no momento, força o engenheiro de depuração a reproduzir o erro para compreendê-lo. Em produção, a reprodução é frequentemente impossível. O registro estruturado captura erros como objetos com campos definidos: carimbo de data/hora no formato ISO 8601, nível de severidade, identificador de erro exclusivo, módulo e função, rastreamento de pilha completo e campos de contexto específicos da operação, como o identificador do usuário, o identificador da solicitação e os parâmetros relevantes para a operação com falha.

Essa estrutura permite consultas ao sistema de registro de logs que não são possíveis com texto de log não estruturado: todos os erros de tempo limite no módulo de pagamentos nos últimos trinta minutos, todos os erros que afetaram solicitações do usuário com ID 12345 nas últimas 24 horas, todos os erros em que o rastreamento de pilha contém uma referência a uma função específica. Essas consultas são o que tornam a análise pós-incidente eficiente.

A mensagem de erro exibida ao usuário é uma questão distinta do registro de log interno. O registro de log deve conter todas as informações necessárias para o diagnóstico. A mensagem exibida ao usuário não deve conter nada que revele detalhes de implementação e deve informar ao usuário o que aconteceu, se ele precisa tomar alguma providência e o que pode fazer caso o problema persista.

Como as plataformas de software devem notificar os usuários sobre erros

A comunicação eficaz de erros para o usuário segue quatro princípios. Primeiro, descreva o problema em termos que o usuário entenda, e não em termos que reflitam a estrutura interna do sistema. “Não foi possível processar seu pagamento neste momento” é preferível a “Reversão da transação: violação de restrição na tabela de pedidos”. Segundo, indique se o problema é temporário ou requer ação do usuário. Uma interrupção temporária do serviço justifica “por favor, tente novamente em alguns minutos”. Um erro de validação justifica “por favor, verifique se o número do seu cartão está correto”. Terceiro, para erros que afetam transações em andamento, confirme explicitamente o estado da transação. Se um pagamento não foi cobrado, diga isso explicitamente. Se o pedido não foi realizado, diga isso explicitamente. A incerteza sobre o estado da transação é uma fonte significativa de desconfiança do usuário. Quarto, forneça um caminho para o suporte caso o usuário não consiga resolver o problema sozinho.

A implementação desses princípios exige que o código de tratamento de erros na interface com o usuário tenha acesso à classificação do erro (para determinar o tipo de mensagem a ser exibida), ao contexto do erro (para tornar a mensagem específica para o que o usuário estava fazendo) e a um sistema de modelos que produza formatos de mensagem consistentes em toda a aplicação.

Design à prova de falhas: Negar acesso quando ocorrerem erros nos controles de segurança.

Um problema de segurança comum causado pelo tratamento inadequado de erros é a verificação de segurança do tipo "fail-open" (falha aberta). Todos os mecanismos de segurança devem negar o acesso até que seja explicitamente concedido, e não conceder acesso até que seja negado, o que é uma razão comum para a ocorrência de erros do tipo "fail-open". Quando uma verificação de autenticação lança uma exceção inesperada, o comportamento correto é negar o acesso. Quando uma verificação de autorização falha ao recuperar as permissões do usuário devido a um erro no banco de dados, o comportamento correto é negar o acesso. Retornar um resultado que concede acesso quando o mecanismo que o negaria falhou é a definição de "fail-open" e está explicitamente listado na categoria A10 da OWASP 2025 como um padrão de vulnerabilidade crítica.

Implementar tratamento de erros à prova de falhas em controles de segurança significa envolver o controle em um manipulador de erros que, por padrão, assume o resultado mais restritivo possível quando qualquer exceção ocorre. Significa nunca usar um bloco `catch` sem tratamento prévio em um contexto sensível à segurança que permita a continuidade da execução. E significa testar os caminhos de erro nos controles de segurança com o mesmo rigor que o caminho de execução bem-sucedido.

Padrões de projeto para tratamento de erros em sistemas distribuídos

Padrão de disjuntor

O padrão de disjuntor impede que falhas em um serviço se propaguem para seus consumidores. Quando uma dependência de serviço excede um limite de taxa de erros definido, o disjuntor é aberto e interrompe o encaminhamento de solicitações para essa dependência, retornando imediatamente uma resposta de erro ou de fallback sem esperar que a dependência responda. Após um período de espera configurável, o disjuntor entra em um estado semiaberto que permite a passagem de um pequeno número de solicitações de teste. Se essas solicitações forem bem-sucedidas, o circuito é fechado e o tráfego normal é retomado. Se falharem, o circuito é reaberto e o período de espera é reiniciado.

Sem disjuntores, uma dependência lenta ou indisponível faz com que os threads do serviço consumidor fiquem bloqueados aguardando respostas que podem nunca chegar. O pool de threads se enche, novas requisições não podem ser processadas e o próprio serviço consumidor fica indisponível para seus chamadores. O disjuntor converte uma falha em cascata em uma falha limitada: a dependência fica indisponível, mas o serviço consumidor permanece operacional e pode atender requisições que não dependem dessa dependência específica.

Padrão de antepara

O padrão bulkhead isola os pools de recursos por dependência, de forma que o esgotamento de um pool não afete as requisições que não utilizam essa dependência. Em um serviço que chama três APIs externas, atribuir um pool de threads separado para cada API significa que uma avalanche de requisições lentas para a API A esgota apenas o pool de threads da API A. As requisições para as APIs B e C continuam sendo processadas normalmente, pois seus pools de threads são independentes.

O limite de isolamento pode ser aplicado no nível do pool de threads, no nível do pool de conexões ou no nível do processo, dependendo da criticidade do isolamento e da sobrecarga que cada abordagem introduz. O princípio, em todos os casos, é o mesmo: a falha de uma dependência não deve ser capaz de consumir recursos necessários para outras dependências.

Padrão Saga para Transações Distribuídas

Em sistemas distribuídos onde uma operação de negócio abrange múltiplos serviços, manter a integridade dos dados quando uma etapa falha requer uma estratégia de compensação. O padrão saga define uma sequência de transações locais, cada uma com uma transação compensatória correspondente que reverte seu efeito. Se a etapa N da saga falhar, a saga executa as transações compensatórias das etapas N-1 a 1 em ordem inversa, restaurando o sistema ao seu estado anterior à saga.

O padrão saga não garante atomicidade no nível do banco de dados: ele alcança consistência eventual por meio de compensação, em vez de rollback. Isso significa que, durante um intervalo de tempo entre o sucesso de uma etapa e a execução de sua compensação, o sistema pode estar em um estado não previsto por nenhuma regra de negócio. O tratamento de erros para cada etapa deve levar isso em consideração: as transações de compensação devem ser idempotentes e o orquestrador da saga deve ser projetado para sobreviver a falhas e retomar a partir do último estado consistente.

Como prevenir o tratamento inseguro de saídas

O tratamento inseguro de saídas no contexto de mensagens de erro é uma das categorias de vulnerabilidade mais exploradas em aplicações web. O padrão de ataque é direto: forçar a aplicação a gerar um erro enviando entradas malformadas, tipos de dados inesperados ou valores limite que acionam caminhos de exceção. Ler a mensagem de erro ou o corpo da resposta HTTP. Extrair os detalhes de implementação revelados. Usar esses detalhes para refinar o ataque.

Para evitar o tratamento inseguro de saídas, é necessário o seguinte:

Nunca inclua detalhes de exceções internas em respostas voltadas para o usuário. O corpo da resposta HTTP, o objeto de erro JSON e a página de erro HTML que um usuário recebe devem conter uma mensagem apropriada para o usuário e, opcionalmente, um código de referência de erro que a equipe de suporte possa usar para consultar a entrada de log interna. Eles nunca devem conter um rastreamento de pilha, uma instrução SQL, um caminho de arquivo, um nome de classe ou uma versão de framework.

Verifique se o código de tratamento de erros foi testado. Os testes unitários para condições de erro devem verificar o que a resposta de erro não contém, bem como o que ela contém. Um teste que confirma que o status da resposta é 500, mas não verifica se o corpo da resposta não contém nenhum rastreamento de pilha, é um teste incompleto para esta vulnerabilidade.

Utilize formatos estruturados de resposta a erros de forma consistente. Um esquema de resposta a erros padronizado, aplicado uniformemente em todos os endpoints, facilita a auditoria das informações retornadas e a garantia de que detalhes internos não sejam incluídos. A formatação ad hoc de respostas a erros é onde ocorrem inconsistências e vazamentos acidentais.

Registre internamente todos os detalhes do diagnóstico. As informações de diagnóstico que não devem estar na resposta exibida ao usuário precisam ser registradas em algum lugar acessível à equipe de engenharia. Um sistema de registro com campos estruturados e controles de acesso apropriados é o destino correto. A chamada de registro e a geração da resposta exibida ao usuário devem ser operações explicitamente separadas no código de tratamento de erros, não compartilhando uma string de mensagem comum.

Um exemplo concreto em Java que demonstra a separação entre o registro de diagnóstico e a resposta voltada para o usuário:

Java

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedError(
        Exception ex, HttpServletRequest request) {

    // Full diagnostic context logged internally; never sent to the user
    String errorId = UUID.randomUUID().toString();
    log.error("Unhandled exception [errorId={}] [path={}] [userId={}]",
            errorId,
            request.getRequestURI(),
            getCurrentUserId(),
            ex);  // full stack trace captured in the log entry

    // User-facing response: error ID for support lookup, no internal details
    ErrorResponse response = new ErrorResponse(
            "An unexpected error occurred. Reference: " + errorId,
            Instant.now()
    );
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

Esse padrão garante que o rastreamento da pilha, a classe da exceção e todo o contexto interno sejam capturados no log, enquanto o usuário recebe apenas um código de referência que a equipe de suporte pode usar para recuperar a entrada de log correspondente.

Análise estática de código para identificar lacunas no tratamento de erros

As falhas no tratamento de erros que mais provavelmente causam incidentes em produção não são as óbvias que os revisores de código detectam. São os padrões estruturais que se acumulam silenciosamente em uma base de código crescente: blocos `catch` vazios que ignoram exceções sem registrá-las, blocos `catch` que registram uma mensagem genérica enquanto descartam a exceção original, valores de retorno de erro que os chamadores não verificam e manipuladores de exceção em trechos de código sensíveis à segurança que permitem que a execução continue mesmo em caso de falha. Esses padrões são invisíveis para os revisores, a menos que estejam procurando especificamente por eles, e em uma base de código grande, revisar todos os blocos `catch` não é viável.

As ferramentas de análise estática de código abordam isso sistematicamente. Sem executar o código, elas analisam o código-fonte, transformando-o em uma árvore sintática abstrata, e consultam essa estrutura em busca de padrões associados ao tratamento incorreto de erros. O SonarQube e ferramentas similares detectam padrões de tratamento de erros inseguros e não confiáveis ​​no código-fonte, incluindo blocos `catch` vazios, rastreamentos de pilha expostos e validação ausente. A análise abrange toda a base de código em uma única passagem, não apenas os arquivos que foram alterados recentemente ou os módulos que causaram incidentes recentemente.

Para sistemas empresariais que combinam diferentes linguagens, a análise deve abranger todas as linguagens presentes no ambiente. Um serviço Java que lida corretamente com erros, mas chama um programa COBOL por meio de uma interface que não propaga erros da camada do mainframe, apresenta uma lacuna no tratamento de erros que uma análise estática focada apenas em Java não consegue detectar. Conforme discutido no contexto de análise estática de código empresarial em diversas linguagensUma análise unificada que abranja todos os idiomas do sistema é o pré-requisito técnico para encontrar falhas no tratamento de erros no nível do sistema, em vez de no nível do arquivo.

Em sistemas legados, o débito de tratamento de erros geralmente se concentra nas partes mais antigas do código, onde as convenções de tratamento de erros foram estabelecidas antes da padronização das práticas modernas. Conforme examinado na análise de Modernização de sistemas legados e tratamento de erros em sistemas herdados.A migração de um sistema de tratamento de erros disperso e inconsistente para uma abordagem centralizada e padronizada é uma tarefa de modernização que se beneficia de ferramentas automatizadas capazes de identificar o estado atual antes que quaisquer alterações sejam feitas.

Como SMART TS XL Aborda o tratamento de erros em escala de sistema

SMART TS XL Constrói um modelo unificado de referência cruzada de todo o ambiente de software, ingerindo código-fonte de todas as linguagens e plataformas, incluindo COBOL, JCL, Java, .NET, Python, JavaScript, TypeScript e SQL, e construindo um índice estrutural que representa as relações entre todos os componentes. Para análise de tratamento de erros, este modelo responde a perguntas que ferramentas de linguagem única não conseguem responder: quais funções em um programa COBOL propagam erros para seus chamadores, quais chamadores dessas funções tratam o erro propagado e quais caminhos pelo sistema podem alcançar uma saída voltada para o usuário sem qualquer tratamento de erros na cadeia de chamadas.

A capacidade de análise de impacto da plataforma estende isso à avaliação de mudanças: antes de modificar o comportamento de tratamento de erros de um componente compartilhado, a análise de impacto identifica todos os outros componentes do sistema que dependem do comportamento atual, para que as mudanças possam ser planejadas e validadas em etapas, em vez de implementadas com consequências desconhecidas a jusante. Esta é a análise descrita no [referência]. soluções de análise de impacto que a IN-COM fornece para ambientes corporativos, aplicada especificamente ao problema de entender o impacto de uma alteração na lógica de tratamento de erros antes que essa alteração seja feita.

SMART TS XLA capacidade de busca corporativa do sistema facilita a análise: uma consulta por todas as funções do sistema que capturam uma exceção sem registrá-la retorna locais de arquivos e nomes de funções específicos, organizados por linguagem e pela gravidade da lacuna, com base em quantas chamadas são feitas para essa função. Essa priorização é o que torna a correção da dívida de tratamento de erros viável, em vez de algo avassalador.

Tratamento de erros como uma propriedade de nível de sistema

O tratamento eficaz de erros não é uma propriedade de módulos individuais isoladamente. Um módulo que trata seus próprios erros corretamente, mas opera dentro de um sistema que não possui registro centralizado de logs, disjuntores em suas dependências externas e um design de transação atômica para suas operações de escrita em múltiplas etapas, ainda assim produzirá incidentes de produção difíceis de diagnosticar. A correção em nível de módulo é necessária, mas não suficiente.

As propriedades de nível de sistema que tornam o tratamento de erros eficaz em toda a aplicação são: classificação consistente de erros, de modo que as condições recuperáveis ​​e irrecuperáveis ​​sejam tratadas de forma diferente em cada camada; registro centralizado, para que todos os eventos de erro sejam capturados em um único sistema consultável com metadados padronizados; disjuntores em todas as dependências externas, para que a falha de uma dependência não esgote os recursos necessários para as outras; design de transação atômica para todas as gravações em várias etapas, para que a conclusão parcial não produza estados inconsistentes; e padrões à prova de falhas em todos os caminhos de código sensíveis à segurança, para que erros nas verificações de controle de acesso neguem, em vez de conceder, o acesso.

Incorporar essas propriedades em um sistema que atualmente não as possui é um trabalho incremental, não um evento único de refatoração. O caminho prático é a análise estática para identificar as lacunas existentes, a priorização dessas lacunas de acordo com seu impacto potencial na estabilidade e segurança e a correção progressiva, começando pelos padrões de maior risco. O resultado final é um sistema onde o tratamento de erros não é algo que os engenheiros precisam considerar para cada nova funcionalidade que escrevem, porque os padrões são padronizados, o framework os impõe e o pipeline de CI verifica se o novo código não introduz os antipadrões que a equipe concordou em eliminar.