A Ansiedade das Assertions

A maioria dos codebases de produção se divide em dois campos. O Campo A trata assert como um tempero decorativo, espalhando-o a cada outra linha até que a função pareça um contrato legal escrito por um advogado paranoico. O Campo B trata assertions como rodinhas de treinamento apenas para desenvolvimento, removendo todas no momento do build e esperando que o código funcione em produção porque os testes passaram uma vez.

Ambos os campos estão errados. A questão não é se devemos fazer assert. A questão é o que uma assertion realmente significa.

Uma assertion não é error handling. Não é input validation. Não é uma sugestão educada. Uma assertion é uma afirmação de que algo é impossível. Se a assertion dispara, seu modelo mental do programa está quebrado. Essa distinção determina tudo sobre onde assertions pertencem e quantas você deve escrever.

Assertions São para Invariants, Não para Erros

Quando um usuário passa uma idade negativa para sua API, isso é um erro. Erros são esperados. Erros merecem real handling, logging e mensagens voltadas para o usuário. Quando seu cálculo interno produz uma contagem negativa de linhas de banco de dados após uma query supostamente bem-sucedida, isso é uma invariant violation. Isso nunca deveria acontecer. É para isso que as assertions existem.

Isso parece óbvio até você ler código de produção. Eu vi funções que afirmam que uma string é não vazia, então três linhas depois verificam if (!str) e lançam uma formatted exception. O desenvolvedor usou ambas as ferramentas para a mesma condição porque nunca decidiu qual delas era o contrato real.

Aqui está a regra. Se a condição pode ser disparada por input externo, não é uma assertion. Se só pode ser disparada por um bug em seu próprio código, é.

def process_payment(user_id: str, amount_cents: int) -> Receipt:
    # NOT an assertion. Users or upstream services can send bad data.
    if amount_cents <= 0:
        raise ValueError("amount_cents must be positive")

    # NOT an assertion. The user_id comes from the outside world.
    if not user_id:
        raise ValueError("user_id is required")

    receipt = _charge_card(user_id, amount_cents)

    # THIS is an assertion. If charge_card returned None after
    # succeeding, our understanding of the universe is wrong.
    assert receipt is not None, "charge_card succeeded but returned None"

    # THIS is an assertion. A receipt with zero items after a
    # successful charge means our internal logic is broken.
    assert len(receipt.items) > 0, "receipt has no items after successful charge"

    return receipt

As duas primeiras verificações protegem o boundary. As duas últimas protegem a consistência interna do sistema. Misturá-las cria confusão sobre quem é responsável pelo quê.

O Limite de Três Assertions

Se você se pegar escrevendo mais de três assertions em uma única função, você tem um de dois problemas. Ou sua função faz coisas demais, ou seus invariants são vagos demais para serem aplicados.

Uma função com doze assertions não é defensiva. É incerta. O autor não confia no código que a chama, no código que ela chama, nem nos dados fluindo entre eles. Essa incerteza deve ser resolvida com refactoring, não adicionando mais assert statements.

O limite prático vem do que um desenvolvedor consegue manter na cabeça. Uma função deve ter um contrato claro. Esse contrato implica um pequeno número de invariants. Se você precisa de uma dúzia de assertions para se sentir seguro, sua função provavelmente absorveu responsabilidades que pertencem a outro lugar.

Divida a função. Extraia a parte que transforma dados. Extraia a parte que chama serviços externos. Dê a cada função extraída seu próprio conjunto pequeno de invariants. Três assertions por função é uma luz de aviso. Cinco é um pneu furado.

Assertions em Produção: Ligadas ou Desligadas?

Diferentes linguagens fazem escolhas diferentes. Python remove assert statements quando você executa com a flag -O. compilers de C e C++ rotineiramente removem assertions em release builds. JavaScript não tem assert embutido. Você ou cria um polyfill ou usa uma biblioteca que permanece ativa em produção.

Isso cria um dilema genuíno. Se você remove as assertions, perde a rede de segurança exatamente quando mais precisa dela. Bugs que só aparecem em produção corromperão dados silenciosamente em vez de falhar rápido. Se você as mantém, arrisca crashar um processo de produção por causa de uma condição que, embora teoricamente impossível, não é realmente fatal.

A resposta depende do custo de continuação. Se violar o invariant significa que a próxima operação corromperá o banco de dados ou vazará dados sensíveis, a assertion deve crashar o processo. Uma parada dura é melhor que uma silent breach. Se violar o invariant significa uma entrada de log ligeiramente errada ou um minor UI glitch, registre e continue.

// This should probably crash. Continuing with a null user
// after auth succeeded is a security hole waiting to happen.
assert(user !== null, "auth middleware returned null user after success");

// This should probably not crash. A stale cache timestamp
// is annoying but not dangerous.
if (cache.timestamp > Date.now()) {
  logger.warn("cache timestamp is in the future, ignoring");
}

Nem todo invariant merece a mesma postura. Aprenda a distinguir entre “isso deve parar” e “isso é estranho, mas sobrevivível.”

O Que Tentamos e Não Funcionou

No início de um projeto, tentamos fazer assert em toda function precondition. Cada argumento era verificado para null, type, range e format. O resultado foi previsível. Os testes passaram lindamente. A produção crashou na primeira vez que uma third-party API retornou um campo como string em vez de number.

O problema não era a assertion. O problema era que fizemos assert em dados fora de nosso controle, então compilamos com assertions habilitadas em produção. Uma malformed external response matou nosso processo em vez de ser sanitizada e tratada. Construímos um sistema que era internamente consistente e externamente frágil.

Aprendemos a separar o boundary do interior. No boundary, parseie e valide agressivamente. Converta o caos externo em certeza interna. Dentro do boundary, afirme os invariants que definem essa certeza. As assertions permaneceram. A input validation mudou para funções de parsing explícitas que retornavam Result types em vez de lançar.

Um Checklist Prático

Antes de adicionar uma assertion, percorra esta lista:

  1. A entrada externa pode disparar isso? Se sim, use validation, não assertion.
  2. Se isso disparar em produção, o processo deve parar? Se não, registre um warning em vez disso.
  3. Esta função já tem três ou mais assertions? Se sim, considere fazer refactoring antes de adicionar outra.
  4. Essa assertion ainda fará sentido para alguém lendo o código daqui a seis meses? Assertions obscuras são deletadas em refactors. As claras sobrevivem.

Assertions são uma ferramenta de comunicação tanto quanto de segurança. Elas dizem ao próximo desenvolvedor, “esta condição é impossível por design.” Se a condição não é realmente impossível por design, a assertion está mentindo. E mentiras em código de produção são caras.

FAQ

Devo fazer assert em argumentos de função?

Somente se o caller também é seu código e o argumento é um produto de lógica interna, não entrada externa. Funções de public API devem validar. Private helper functions podem afirmar invariants sobre os valores que recebem.

E o TypeScript? Ele já captura nulls em compile time.

O type system do TypeScript é uma poderosa camada de assertion, mas ele desaparece em runtime. Use-o para tudo que o compiler pode provar. Adicione runtime assertions para as lacunas: API responses, deserialized data e qualquer cast as que contorne o type checker.

As assertions prejudicam a performance?

Na maioria das linguagens, uma assertion bem colocada custa microssegundos. Se você está fazendo assert dentro de um tight loop processando milhões de itens, mova a assertion para fora do loop. Verifique o invariant no batch, não em cada elemento.

Devo escrever funções de assert customizadas?

Somente quando a built-in assertion message não seria útil. Uma assertNonEmpty customizada que imprime o comprimento real do array é mais útil que um assert len(items) > 0 genérico que crasha sem contexto. Mantenha-as pequenas. Não construa um assertion framework.