Tem uma cena que quase todo mundo que começa com Apex conhece.

Você está desenvolvendo, parece que está tudo indo bem, executa o código e, de repente:

System.NullPointerException
System.DmlException
System.QueryException
Too many SOQL queries: 101
Apex CPU time limit exceeded

Pronto.

A tela fica vermelha, o teste falha, o deploy não passa ou o Debug Log vira uma parede de texto.

E aí vem aquele pensamento:

"Tá. Mas o que exatamente eu quebrei?"

Se isso já aconteceu com você, este artigo é para esse momento.

Não quero que você saia daqui decorando vinte nomes de exception. Quero uma coisa bem mais útil: que, quando um erro aparecer, você consiga olhar para ele e pensar "ok, eu sei por onde começar".

Porque essas mensagens têm uma coisa em comum: todas dizem que alguma coisa deu errado.

Mas só isso.

Elas não estão falando do mesmo problema.

Uma pode estar dizendo que seu código nem conseguiu chegar à execução. Outra aparece porque o código começou a rodar e encontrou um estado inesperado. E outra pode significar que sua transação simplesmente pediu à plataforma mais recursos do que ela permite.

Pensa no painel de um carro.

Uma luz pode estar avisando que a porta está aberta. Outra pode estar dizendo que o combustível está acabando. Outra pode indicar um problema sério no motor.

Todas acendem no painel.

Mas você não levaria o carro para trocar o motor porque a porta ficou aberta, certo?

Com Apex é a mesma coisa.

O primeiro passo não é sair colocando try/catch em tudo.

O primeiro passo é descobrir que tipo de alerta você está olhando.

E é exatamente isso que vamos fazer juntos.

Antes de começar, guarda este mapa

Ao longo do artigo, você vai ver muitos nomes, exemplos e cenários. Quando começar a parecer muita coisa, volte para este mapa:

PROBLEMA EM APEX
│
├── Compile-time error
│      O código não consegue ser compilado ou validado.
│
├── Runtime exception
│      O código começou a executar, mas encontrou um problema.
│
└── Governor Limit
       A transação consumiu mais recursos
       do que a plataforma permite.

Só essa separação já muda bastante a forma de investigar.

Se o código nem começou, você pensa em compilação, tipos, referências e estrutura.

Se começou e tropeçou no caminho, você pensa em exception, dados, estado e fluxo.

Se bateu em Governor Limit, a pergunta muda completamente: o que na arquitetura da transação está consumindo recurso demais?

Vamos por partes.

No dia a dia, a gente chama praticamente tudo de "erro". E está tudo bem. O problema aparece quando essa palavra genérica começa a atrapalhar o diagnóstico.

Para investigar Apex sem ficar atirando para todos os lados, vale separar os conceitos.

Compile-time error

O Salesforce analisa o código e conclui que ele não é válido para execução.

O programa nem chega a executar normalmente.

Runtime exception

O código é válido e começa a executar, mas encontra uma situação que impede a operação de continuar normalmente.

Por exemplo:

String text;

System.debug(text.toUpperCase());

O código é sintaticamente válido.

Mas text contém null.

Quando Apex tenta executar:

text.toUpperCase()

ocorre uma:

System.NullPointerException

A própria documentação da Salesforce utiliza esse tipo de situação para explicar tratamento de erros Apex e recomenda validação de valores potencialmente nulos, incluindo o uso do safe navigation operator quando apropriado.

Governor limit

O código também começa a executar.

Porém, durante aquela transação, ele consome mais de determinado recurso do que o Salesforce permite.

Por exemplo:

System.LimitException:
Too many SOQL queries: 101

Aqui o problema não é simplesmente uma consulta que retornou algo inesperado.

A transação realizou consultas demais.

Governor Limits fazem parte da arquitetura multitenant do Salesforce e existem para impedir que uma única execução monopolize recursos compartilhados da plataforma.


Parte 1: Antes de tentar corrigir, descubra em que momento tudo deu errado

Tem uma pergunta que resolve metade da confusão:

O código chegou a executar?

Parece básico, mas essa pergunta separa dois mundos.

Se a resposta for "não", não adianta procurar valor de variável, Debug Log de runtime ou catch. Se a resposta for "sim", aí começamos a investigar estado, dados e caminho de execução.

Quando o código nem consegue sair da garagem: compile-time errors

Aqui vale uma imagem mental bem simples: o código ainda está na porta.

Ele não entrou na execução, não consultou registro, não fez DML e não chegou a uma linha que pudesse lançar uma exception de runtime. A plataforma está dizendo: "eu ainda não consigo transformar isso em algo executável".

Por isso try/catch não ajuda. O try/catch só existe durante a execução. Se a execução nunca começou, não existe bloco de tratamento rodando para capturar nada.

Esse detalhe parece pequeno, mas evita uma confusão muito comum de quem está começando.

Antes de qualquer classe ou trigger fazer alguma coisa de verdade, o código precisa passar pela primeira barreira: ele precisa ser válido.

Considere:

Integer quantity = 'ten';

quantity foi declarado como Integer, mas estamos tentando atribuir uma String.

Esse é o tipo de problema que precisa ser corrigido no código antes que a lógica possa executar.

Outro exemplo:

Account acc = new Account();

acc.methodThatDoesNotExist();

O método não existe para aquele tipo.

Outro:

System.debug(customerName);

se customerName nunca tiver sido declarada naquele escopo.

Entre os problemas que o desenvolvedor pode encontrar nessa fase estão:

  • sintaxe inválida;
  • tipos incompatíveis;
  • variável inexistente;
  • método inexistente;
  • assinatura de método incompatível;
  • referência inválida a classes, propriedades ou métodos;
  • problemas estruturais no código;
  • exceder o tamanho máximo permitido para uma classe, trigger, ou para o total de código Apex do org: esse tipo de problema também impede o deploy, mesmo não sendo um erro de sintaxe.

A característica importante para o iniciante é:

Se existe um erro de compilação (ou de deploy), não adianta adicionar try/catch.

O problema precisa ser corrigido para que o código possa existir no org.


Parte 2: Quando o código entra em execução e a realidade não coopera

Agora estamos dentro da execução.

E aqui mora uma verdade importante sobre software: o código pode estar perfeitamente válido e ainda assim encontrar um mundo bem diferente daquele que você imaginou enquanto desenvolvia.

Um registro pode não existir. Uma lista pode vir vazia. Uma integração pode devolver outro formato. Uma validação pode impedir um insert. Um valor pode estar null.

É nesse território que aparecem muitas das runtime exceptions.

O código começou a rodar. E aí alguma coisa aconteceu

Agora a cena muda.

O código passou pela etapa anterior, começou a rodar e encontrou uma situação que a lógica não conseguiu concluir normalmente. É aqui que try, catch, finally, custom exceptions e estratégias de tratamento passam a fazer sentido.

Mas cuidado com uma tentação: descobrir que exceptions podem ser capturadas e concluir que a solução profissional é colocar try/catch em todo lugar.

Não é.

Tratamento de erro não é uma rede jogada sobre o código inteiro. Ele funciona melhor quando você consegue dizer qual falha está esperando, por que ela pode acontecer e o que o sistema deve fazer se ela realmente acontecer.

Agora sim entramos naquele cenário que costuma confundir mais: o código compilou, começou a executar e só então encontrou alguma coisa que não conseguiu resolver normalmente.

A Salesforce documenta diversas exceptions Apex nativas e permite que você capture exceptions esperadas usando try/catch. Também é possível definir suas próprias custom exceptions.

A estrutura básica é:

try {

    // Código que pode gerar uma exceção.

} catch (Exception e) {

    // O que fazer se a exceção ocorrer.
}

Mas isso não significa que todo código deve ficar dentro de um gigantesco try/catch.

A recomendação da própria Salesforce é tratar os erros que você espera e separar blocos quando diferentes partes da lógica exigem tratamentos diferentes.

Este artigo cobre as exceções nativas mais comuns em detalhe: as que você vai encontrar quase toda semana: e depois reúne, num bloco próprio, as que aparecem com menos frequência, mas que também fazem parte do vocabulário oficial da linguagem.


NullPointerException: o clássico "eu achei que tinha alguma coisa aqui"

Se existe uma exception que quase todo mundo encontra cedo em Apex, é esta.

E ela assusta mais pelo nome do que pela ideia. No fundo, a plataforma está dizendo: "você tentou usar alguma coisa que, naquele momento, não apontava para um objeto válido."

A parte mais importante não é adicionar um if (x != null) e seguir em frente. O ganho real vem de perguntar: por que esse valor chegou null até aqui?

Às vezes null é normal. Um campo opcional pode estar vazio. Uma busca pode não encontrar um relacionamento. Um parâmetro pode ser opcional.

Em outros casos, null denuncia uma premissa quebrada. Talvez a lógica jurasse que uma variável seria preenchida em uma etapa anterior, mas aquela etapa não aconteceu.

Então pense em NullPointerException como duas perguntas, não uma:

  1. Qual referência está null?
  2. Ela poderia estar null nesse ponto ou isso revela um problema no fluxo?

Se você desenvolve em Apex por tempo suficiente, esse nome acaba ficando familiar. Quase todo mundo conhece a sensação de olhar para NullPointerException e pensar: "mas de onde veio esse null?"

Considere:

Account acc;

System.debug(acc.Name);

Nenhum objeto foi atribuído a acc.

Portanto:

acc == null

Ao tentar acessar:

acc.Name

o código tenta utilizar um objeto que não existe naquele momento.

Pode ocorrer:

System.NullPointerException:
Attempt to de-reference a null object

Como pensar nisso

Imagine que uma variável é uma caixa.

Você escreveu:

Account acc;

Você criou a caixa.

Mas ainda não colocou uma Account dentro dela.

Depois tentou:

acc.Name

É como perguntar o nome da Account que deveria estar dentro de uma caixa vazia.

Prevenção

Uma opção:

if (acc != null) {
    System.debug(acc.Name);
}

Apex também suporta safe navigation:

String accountName = acc?.Name;

A Salesforce recomenda null checks, inicialização adequada das variáveis e, quando apropriado, safe navigation para evitar dereferenciamento de objetos null.


DmlException: o código tentou salvar, mas o Salesforce disse "não"

Agora entramos no banco de dados.

Uma DmlException não quer dizer simplesmente "Salesforce não gosta do meu código". Ela quer dizer que uma operação de alteração de dados foi tentada e não conseguiu ser concluída conforme as regras aplicáveis naquele momento.

Essa distinção é importante porque o código pode estar perfeitamente compilado e a instrução insert, update, delete ou upsert pode estar escrita corretamente. O problema pode estar nos dados e nas regras que cercam aqueles dados.

Quando encontrar uma DmlException, acostume-se a investigar a mensagem completa. Ela frequentemente traz muito mais contexto do que apenas o nome da exception.

Vamos sair um pouco da memória e ir para o banco de dados. DML é o que usamos quando queremos modificar registros.

Por exemplo:

insert account;
update contact;
delete opportunity;
upsert customer;

Uma operação DML pode falhar.

Exemplo simplificado:

Account acc = new Account();

insert acc;

Se alguma exigência de dados aplicável à operação não for satisfeita, o insert pode falhar.

Apex pode lançar:

System.DmlException

A Salesforce documenta que operações DML que falham lançam DmlException. Isso pode ocorrer, por exemplo, quando os dados são inválidos ou alguma validação acionada durante a transação impede a operação.

Podemos tratá-la:

try {

    insert acc;

} catch (DmlException e) {

    System.debug(e.getMessage());
}

Agora vem uma confusão muito comum: DmlException não é DML Governor Limit

Esta é uma daquelas diferenças que valem uma pausa.

Imagine duas situações:

  • você tentou fazer um insert e aquele insert falhou;
  • você fez operações DML demais dentro da mesma transação.

Visualmente, ambas envolvem DML. Arquiteturalmente, são problemas completamente diferentes.

Na primeira, investigamos por que a operação não foi aceita. Na segunda, investigamos por que o desenho da transação consumiu DML demais.

Esse tipo de separação é o coração deste artigo.

Essa diferença merece atenção porque as duas mensagens têm "DML" no contexto e, olhando rápido, parecem parentes próximas. Só que elas pedem investigações completamente diferentes.

Considere:

insert accounts;

Você pode executar apenas um DML statement e receber:

System.DmlException

porque algum problema ocorreu durante aquela operação.

Isso é diferente de:

System.LimitException:
Too many DML statements: 151

Aqui o problema é que a transação executou operações DML demais.

Veja a diferença:

Situação Significado
DmlException Uma operação DML falhou
Too many DML statements A transação excedeu o Governor Limit de DML

Essa distinção é fundamental. Quando ela fica clara, você para de tentar resolver problema de consumo de recurso com ferramenta de tratamento de exception.

DML exception fala sobre o sucesso ou fracasso da operação.

DML Governor Limit fala sobre consumo de recursos da transação.

A Salesforce estabelece um limite de 150 DML statements por transação no contexto padrão discutido na documentação de bulkificação.


QueryException: e se a sua consulta não devolver o que você imaginou?

SOQL é um daqueles lugares em que uma suposição aparentemente inocente vira uma exception bem rápido.

O banco de dados não tem obrigação de devolver exatamente o cenário que imaginamos quando escrevemos o código. O mundo real tem registros ausentes, duplicidades, dados antigos, integrações atrasadas e caminhos de negócio que nem sempre foram previstos.

Por isso, uma das perguntas mais úteis ao ler uma QueryException é:

O problema está na consulta ou na expectativa que meu código criou sobre o resultado da consulta?

Essa mudança de perspectiva ajuda bastante.

Considere:

Account acc = [
    SELECT Id, Name
    FROM Account
    WHERE Name = 'Example'
];

Observe uma coisa importante:

O resultado está sendo colocado diretamente em:

Account acc

Isso pressupõe que a consulta produzirá um resultado compatível com essa atribuição.

Se nenhum registro for retornado, você pode encontrar:

System.QueryException:
List has no rows for assignment to SObject

Existem outros cenários que também podem resultar em QueryException.

A Salesforce cita explicitamente QueryException entre as exceções Apex que um desenvolvedor pode precisar tratar ao trabalhar com consultas.

Se zero registros é uma possibilidade normal do negócio, uma alternativa mais resiliente é trabalhar com uma coleção:

List<Account> accounts = [
    SELECT Id, Name
    FROM Account
    WHERE Name = 'Example'
];

if (!accounts.isEmpty()) {
    Account acc = accounts[0];
}

Agora zero registros não é necessariamente uma exceção.

Pode simplesmente ser um resultado esperado da consulta.

Isso mostra um princípio importante:

Nem todo problema precisa ser resolvido com catch. Muitas exceções são melhor prevenidas pelo desenho adequado do código.


SObjectException: o registro existe, mas você pediu algo que não estava disponível

Aqui temos um caso interessante: existe um objeto, mas isso não significa que tudo sobre ele esteja disponível da forma que seu código deseja.

Em Apex, especialmente quando trabalhamos com resultados de SOQL, precisamos lembrar que carregamos os campos que pedimos. Se a lógica tenta usar um campo que não veio naquela consulta, o problema não é "registro inexistente". É a forma como o objeto foi preparado e utilizado.

Considere:

Account acc = [
    SELECT Id
    FROM Account
    LIMIT 1
];

System.debug(acc.Name);

A consulta buscou:

Id

mas não buscou:

Name

Você tenta posteriormente acessar Name.

Esse tipo de uso pode produzir:

System.SObjectException

com uma mensagem indicando que o campo solicitado não foi consultado.

A lição para o iniciante é simples:

Quando Apex retorna um SObject por SOQL, consulte os campos que sua lógica realmente precisa utilizar.

Também não significa:

SELECT todos_os_campos

sempre.

Isso aumenta desnecessariamente o volume de dados carregado.

A ideia é buscar os campos necessários para o caso de uso.


ListException: a lista existe. O elemento que você quer, talvez não

Observe como isso é diferente de null.

E aqui tem uma diferença que parece pequena até o dia em que ela salva seu debugging: uma lista vazia continua sendo uma lista perfeitamente válida. Ela existe, pode ser inspecionada e pode representar corretamente um resultado de negócio: "não encontramos nenhum registro".

O erro aparece quando o código trata "lista existente" como se significasse automaticamente "lista com pelo menos um elemento".

É uma diferença pequena na leitura, mas enorme no debugging.

Considere:

List<Account> accounts = new List<Account>();

Account firstAccount = accounts[0];

A lista existe.

Mas ela está vazia.

Portanto, não existe elemento na posição 0.

O código pode gerar:

System.ListException:
List index out of bounds: 0

Uma prevenção simples:

if (!accounts.isEmpty()) {
    Account firstAccount = accounts[0];
}

A diferença para NullPointerException é interessante.

Aqui:

accounts

não é null.

A lista existe.

O problema é que estamos tentando acessar uma posição inexistente.


TypeException: quando o valor não é do tipo que você jurava que era

Quando dados percorrem várias camadas, a sensação de que "eu sei o tipo desse valor" pode enganar.

Isso aparece bastante com Object, payloads dinâmicos, desserialização, maps genéricos e integrações. Então, antes de forçar um cast, vale conferir o que realmente chegou.

Em outras palavras: não negocie com o tipo que você gostaria de ter; investigue o tipo que você realmente tem.

Considere:

Object value = 'hello';

Integer numberValue = (Integer) value;

O objeto contém uma String.

Estamos tentando tratá-lo como Integer.

Esse tipo de operação incompatível pode resultar em:

System.TypeException

Você encontra situações semelhantes ao trabalhar com:

  • objetos genéricos;
  • casts;
  • dados dinâmicos;
  • desserialização;
  • estruturas que chegam de outros pontos da aplicação.

A pergunta útil é:

Qual é realmente o tipo do valor que chegou aqui?


StringException: nem toda String pode virar qualquer coisa

Um exemplo bastante conhecido envolve Salesforce IDs.

Id accountId = 'this-is-not-an-id';

A string não representa um Salesforce ID válido.

A operação pode gerar:

System.StringException:
Invalid id

O código recebeu uma String.

Mas isso não significa que qualquer String possa ser convertida para qualquer outro tipo.


MathException: sim, até uma conta simples pode quebrar em runtime

Considere:

Integer total = 10;
Integer divisor = 0;

Integer result = total / divisor;

Divisão por zero não é uma operação matemática válida.

Apex pode gerar:

System.MathException:
Divide by 0

Isso mostra novamente a diferença entre compilação e runtime.

O código:

total / divisor

é perfeitamente válido do ponto de vista sintático.

O problema aparece somente quando:

divisor == 0

durante a execução.


JSONException: quando o payload chega diferente do combinado

JSON vindo de fora da sua aplicação merece uma dose extra de desconfiança saudável.

Não porque o outro sistema esteja "errado", mas porque contratos de integração mudam, campos podem se tornar opcionais, respostas de erro podem ter formato diferente da resposta de sucesso e valores podem chegar nulos onde você não esperava.

Uma integração resiliente trata o payload como entrada a ser validada, não como promessa de perfeição.

Integrações frequentemente trabalham com JSON.

Por exemplo:

CustomerResponse response =
    (CustomerResponse) JSON.deserialize(
        jsonBody,
        CustomerResponse.class
    );

Se os dados recebidos não puderem ser processados como esperado, operações de serialização ou desserialização podem gerar exceções relacionadas a JSON.

O princípio para iniciantes é:

Dados externos não devem ser considerados automaticamente válidos.

Uma integração precisa considerar:

  • estrutura recebida;
  • campos opcionais;
  • tipos de dados;
  • valores null;
  • payload inválido;
  • respostas inesperadas.

CalloutException: agora existe outro sistema na conversa

Callout adiciona uma variável que seu código não controla sozinho: outro sistema.

Você pode escrever um código correto e, ainda assim, enfrentar timeout, indisponibilidade, configuração incorreta de endpoint, problemas de autenticação ou respostas inesperadas.

Por isso o tratamento de callouts precisa separar duas coisas:

  • a falha de uma chamada específica;
  • o consumo dos limites de callout da transação.

De novo, exception e Governor Limit podem aparecer perto do mesmo assunto sem significar a mesma causa.

Apex pode realizar chamadas HTTP para sistemas externos.

Por exemplo:

Http http = new Http();
HttpRequest request = new HttpRequest();

request.setEndpoint('callout:ExampleService/resource');
request.setMethod('GET');

HttpResponse response = http.send(request);

Problemas relacionados à execução de callouts podem resultar em:

System.CalloutException

Callouts possuem ainda regras e limites próprios da plataforma, portanto é importante separar novamente dois conceitos:

CalloutException

não é automaticamente a mesma coisa que:

Governor Limit de callouts

Uma chamada pode falhar individualmente sem que o número máximo permitido de callouts tenha sido alcançado.


E aquelas exceptions que você quase nunca vê?

As exceptions acima cobrem boa parte do que você tende a encontrar com frequência. A documentação oficial da Salesforce também reúne outras exceptions nativas que aparecem em contextos mais específicos. Você não precisa decorá-las agora: mas precisa reconhecer o nome quando aparecer, em vez de ficar sem saber o que fazer.

Separei por "família", porque isso ajuda a memorizar.

Família 1: Ligadas a Visualforce

Se você trabalha só com LWC, é bem provável que nunca esbarre nessas. Mas se o seu org ainda tem páginas Visualforce (e muitos têm), elas aparecem.

VisualforceException: qualquer problema relacionado a uma página Visualforce em si, por exemplo um erro na renderização ou no binding de um componente.

NoAccessException: você tentou acessar, via Visualforce, um sObject ou campo que o usuário atual não tem permissão de ver.

// Em uma extension de VF controller
Account acc = [SELECT Id, Name FROM Account LIMIT 1];
// Se o usuário não tiver FLS/sharing para Account,
// o acesso pode disparar NoAccessException

NoDataFoundException: você tentou acessar um registro que já não existe mais, por exemplo, foi deletado entre a hora que a página carregou e a hora que o usuário clicou em algo.

SerializationException: problema ao serializar dados, geralmente em contexto de Visualforce, quando algo não consegue ser convertido corretamente para o formato esperado pela página.

Família 2: Infraestrutura e execução assíncrona

AsyncException: algo deu errado ao tentar enfileirar uma operação assíncrona, por exemplo, uma chamada a System.enqueueJob() que falhou.

try {
    System.enqueueJob(new MinhaQueueable());
} catch (AsyncException e) {
    System.debug('Falha ao enfileirar o job: ' + e.getMessage());
}

ExternalObjectException: problema ao acessar dados armazenados em sistemas externos via External Objects (Salesforce Connect), por exemplo, um timeout de conexão com o sistema externo.

FinalException: duas situações comuns disparam essa:

  1. Você tentou modificar um registro em modo somente leitura. O exemplo clássico é tentar editar Trigger.new dentro de um trigger after insert ou after update. Nesse ponto, o registro já foi escrito (mas não commitado) no banco, e por isso é read-only.
trigger ContaTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        acc.Name = 'Novo Nome'; // FinalException:
                                  // Record is read-only
    }
}
  1. Você tentou reatribuir uma variável declarada como final.

Prevenção: se você precisa alterar o registro no mesmo fluxo, faça isso no before insert/before update (onde ele ainda é editável), ou consulte o registro novamente do banco antes de atualizar.

Família 3: Comunicação e dados

EmailException: qualquer problema relacionado a e-mail, por exemplo, falha ao entregar um e-mail disparado via Messaging.sendEmail().

XmlException: problema ao ler ou escrever XML usando as classes XmlStreamReader / XmlStreamWriter.

SearchException: problema em uma consulta SOSL executada via chamada SOAP API search(), por exemplo, quando o parâmetro de busca tem menos de dois caracteres.

SecurityException: problema com os métodos estáticos da classe utilitária Crypto, usada para criptografia, assinatura digital e geração de hash.

Família 4: "isso não deveria ter acontecido"

NoSuchElementException: você tentou acessar um elemento de um Iterator além do que existe. O padrão clássico é chamar .next() quando .hasNext() já retornou false.

Iterator<Account> it = accounts.iterator();
while (it.hasNext()) {
    Account acc = it.next();
}
// Chamar it.next() de novo aqui geraria NoSuchElementException

InvalidParameterValueException: um parâmetro inválido foi passado para um método, por exemplo, um valor de header HTTP reservado sendo usado onde não deveria.

RequiredFeatureMissing: um recurso do Chatter é exigido pelo código, mas o org de destino não tem Chatter habilitado. Costuma aparecer em cenários de deploy entre orgs com configurações diferentes.

Uma observação sobre completude

Mesmo reunindo as exceptions detalhadas antes com as desta seção, esta lista não deve ser tratada como uma fotografia eterna de 100% do universo de exceptions Apex: a plataforma evolui e pode introduzir novas exceptions em releases futuras. Na dúvida, confirme contra a documentação oficial "Built-In Exceptions and Common Methods" do Apex Developer Guide.


AssertException: o teste ficou vermelho. Isso não quer dizer automaticamente que ele está errado

Eu sei que teste vermelho irrita, principalmente quando você só queria terminar o deploy. Mas ele não é seu inimigo. Ele está tentando contar uma história.

Uma AssertException diz que existe uma diferença entre o comportamento esperado pelo teste e o comportamento realmente observado. Só isso.

O próximo passo é investigar qual lado dessa comparação está errado.

Pode ser o código de produção. Pode ser o dado preparado no teste. Pode ser uma regra de negócio que mudou e deixou uma assertion antiga para trás. Pode ser uma expectativa mal formulada desde o início.

A pior reação é alterar a assertion apenas até ela ficar verde.

Em testes Apex, fazemos afirmações sobre o comportamento esperado do código.

Por exemplo:

System.assertEquals(
    10,
    result
);

Se result for 8, o teste precisa falhar.

Isso é desejável.

Um teste serve justamente para revelar quando:

resultado real != resultado esperado

Uma falha de assertion não significa necessariamente que o próprio teste está errado.

Ela significa que você precisa investigar:

  1. O comportamento esperado está correto?
  2. O dado preparado pelo teste está correto?
  3. O código em produção está retornando algo incorreto?
  4. A assertion está testando a regra certa?

Nunca altere uma assertion simplesmente para transformar um teste vermelho em verde sem compreender a causa da diferença.


Custom Exceptions: quando o seu negócio precisa dar nome ao problema

Nem todo problema do sistema se encaixa bem em:

DmlException
QueryException
NullPointerException

Imagine uma regra de negócio:

Este cliente não pode continuar porque não atende
aos critérios do processo.

Você pode definir sua própria exception:

public class CustomerNotEligibleException extends Exception {
}

E lançá-la quando necessário:

if (!isEligible) {
    throw new CustomerNotEligibleException(
        'Customer is not eligible for this process.'
    );
}

A Salesforce permite a criação de custom exceptions estendendo Exception. Isso ajuda a representar situações específicas da aplicação de forma mais clara.


AuraHandledException: o usuário não precisa receber o seu stack trace

Agora pensa na pessoa que está usando a tela. Ela não precisa falar a mesma língua que o seu backend.

A equipe de desenvolvimento precisa de detalhes técnicos para diagnosticar uma falha. Quem está usando o sistema precisa saber o que aconteceu em termos úteis para conseguir continuar o trabalho.

É aqui que uma estratégia de erro bem desenhada fica mais humana: registrar internamente o que a engenharia precisa, enquanto o cliente recebe uma mensagem controlada e compreensível.

Isso não é esconder o problema. É separar observabilidade técnica de experiência do usuário.

Imagine um método Apex chamado por um Lightning Web Component.

Você não quer necessariamente enviar ao usuário final detalhes internos como:

Class.CustomerController line 87
System.NullPointerException...

Em determinados cenários, você pode retornar uma mensagem controlada:

throw new AuraHandledException(
    'We could not complete the operation.'
);

Apesar do nome, AuraHandledException também é suportada por Lightning Web Components.

A documentação da Salesforce explica que ela permite retornar uma mensagem personalizada ao cliente sem expor o stack trace da mesma maneira que uma exception não tratada.

Isso não significa esconder problemas técnicos.

O sistema pode registrar detalhes técnicos para diagnóstico enquanto apresenta ao usuário uma mensagem adequada ao contexto.


Tratada ou não tratada: o que acontece depois que a exception aparece?

Agora podemos fazer outra divisão importante, desta vez pensando no que o sistema faz depois que a exception aparece.

Handled exception

Seu código esperava que aquela situação pudesse ocorrer e definiu um comportamento.

try {

    insert accounts;

} catch (DmlException e) {

    System.debug(
        'The records could not be inserted: ' +
        e.getMessage()
    );
}

Unhandled exception

Nenhum código captura a exceção.

Ela continua propagando até interromper a execução correspondente.

Quando Apex é chamado por um componente Lightning, uma exception não tratada pode chegar ao cliente com informações do erro. A Salesforce recomenda controlar quais erros devem ser tratados no servidor e quais informações realmente devem ser enviadas ao cliente.


"Então eu coloco um catch genérico e pronto?" Não exatamente

Eu entendo a tentação. Você descobre que catch (Exception e) captura um monte de coisas e pensa: "ótimo, resolvi meu problema".

Afinal, se um catch genérico consegue interceptar várias exceptions, por que não colocar um desses em volta de tudo e chamar de resiliência?

Porque um sistema que "captura tudo" pode acabar entendendo nada.

Um bom tratamento não existe apenas para impedir uma tela vermelha. Ele existe para tomar uma decisão coerente diante de uma falha conhecida. Quando o catch é amplo demais, você corre o risco de tratar situações diferentes como se fossem iguais, perder informação útil e esconder bugs que deveriam subir até uma camada capaz de lidar com eles.

Não. E entender o motivo é mais útil do que simplesmente decorar essa resposta.

Tecnicamente, um catch genérico pode capturar diversas exceções comuns:

try {

    // logic

} catch (Exception e) {

    // handling
}

Mas isso não torna automaticamente o código robusto.

Compare:

try {

    insert accounts;

} catch (DmlException e) {

    // tratamento específico para falha de DML
}

com:

try {

    insert accounts;

} catch (Exception e) {

    // qualquer coisa
}

No primeiro caso, o código deixa clara a situação esperada.

A Salesforce recomenda que os catch blocks considerem as exceções relevantes ao código executado e que apenas os erros esperados sejam tratados ali, propagando os demais quando necessário.

E existe outro motivo pelo qual catch (Exception e) não resolve tudo.

Governor Limit exceptions não são capturadas dessa maneira.


Parte 3: Quando o problema não é um dado inesperado, mas o quanto sua transação está consumindo

Até aqui a conversa foi muito sobre "alguma coisa inesperada aconteceu durante a execução".

Governor Limits contam outra história.

Aqui o Salesforce não está dizendo que um registro específico estava errado. Está dizendo que aquela transação ultrapassou uma fronteira de recursos.

Essa diferença muda completamente o que você faz em seguida.

Agora chegamos aos Governor Limits, e aqui a lógica muda

Agora muda o jogo de verdade.

Até agora estivemos falando principalmente de código que encontra situações excepcionais durante a execução. Governor Limits têm outra função: eles protegem os recursos compartilhados da plataforma.

Pense em um prédio em que muitas empresas dividem infraestrutura. Não seria sustentável permitir que uma única sala consumisse toda a energia, toda a água e todos os elevadores sem nenhum controle. O Salesforce aplica limites pelo mesmo princípio geral: uma execução não pode monopolizar recursos em um ambiente multitenant.

Por isso a mentalidade correta não é "como eu capturo esse limite depois que ele estourar?". É "como eu desenho a transação para não chegar a esse ponto?"

Agora a diferença entre falha de operação e consumo excessivo de recurso fica muito mais clara.

Uma:

DmlException

pode representar uma operação que falhou.

Uma:

NullPointerException

pode representar uma tentativa de usar um objeto null.

Mas:

Too many SOQL queries: 101

significa que a própria transação ultrapassou uma fronteira de recursos imposta pelo Salesforce.

Governor Limits existem porque Salesforce executa workloads em uma arquitetura multitenant e precisa proteger recursos compartilhados entre diferentes clientes.


Quais limites você realmente precisa conhecer no começo?

E, antes que essa parte pareça uma tabela infinita para decorar: você não precisa memorizar todos os limites da plataforma no primeiro dia.

Comece reconhecendo as famílias. Pergunte se sua transação está consumindo demais em consultas, linhas retornadas, DML, linhas DML, CPU, heap ou chamadas externas.

Depois, quando estiver implementando ou diagnosticando algo real, confirme o valor vigente na documentação oficial para o contexto daquela execução.

Isso é mais saudável do que transformar números em dogma, especialmente porque alguns limites variam entre contexto síncrono e assíncrono e podem evoluir entre releases.

Salesforce possui muitos limites, organizados em categorias:

  • Limites por transação: recomeçam do zero a cada transação (e, no caso de Batch Apex, a cada chunk).
  • Limites estruturais/tamanho de código: não são por transação, mas sim sobre o próprio código, e bloqueiam o deploy quando excedidos.
  • Limites de plataforma: não são específicos de uma transação Apex, são impostos pela plataforma como um todo.

Os primeiros que eu aprenderia: e que o iniciante mais vai encontrar: são:

Limite Categoria O que controla
SOQL Queries Transação Quantas consultas são executadas
SOQL Query Rows Transação Quantos registros são retornados, somando todas as consultas
DML Statements Transação Quantas operações DML são executadas
DML Rows Transação Quantos registros são processados por DML, somando todas as operações
CPU Time Transação Quanto tempo de CPU a transação consome
Heap Size Transação Quanta memória Apex está utilizando
Callouts Transação Quantas chamadas externas são realizadas
E-mails Transação Quantas invocações de envio de e-mail são feitas
Future Methods Transação Quantos métodos @future podem ser invocados
Queueable Jobs Transação Quantos jobs podem ser enfileirados via System.enqueueJob()
Tamanho de código Apex Estrutural Tamanho máximo de classes, triggers e do código total do org

Salesforce aplica diferentes categorias de limites, incluindo CPU, heap e limites por transação. Alguns valores também variam conforme o contexto de execução, principalmente entre execução síncrona e assíncrona.


SOQL: não é só quantas queries você faz

Um exemplo clássico de estourar o número de consultas:

for (Account acc : accounts) {

    List<Contact> contacts = [
        SELECT Id
        FROM Contact
        WHERE AccountId = :acc.Id
    ];
}

Se o loop fizer consultas suficientes, você pode encontrar:

System.LimitException:
Too many SOQL queries: 101

No contexto síncrono comum, a Salesforce estabelece um máximo de 100 consultas SOQL por transação.

A solução não é:

try {
    // queries
} catch (Exception e) {
    // save me
}

A solução é arquitetar a consulta corretamente.

Por exemplo:

Set<Id> accountIds = new Set<Id>();

for (Account acc : accounts) {
    accountIds.add(acc.Id);
}

List<Contact> contacts = [
    SELECT Id, AccountId
    FROM Contact
    WHERE AccountId IN :accountIds
];

Uma consulta trabalha com o conjunto de registros.

Esse princípio é chamado de bulkification.

A Salesforce recomenda trabalhar com coleções e evitar SOQL e DML dentro de loops como prática fundamental para respeitar Governor Limits.

Mas existe um segundo limite, separado desse: o total de registros retornados por todas as consultas somadas em uma transação: algo na casa de 50.000 linhas. Você pode fazer poucas consultas e ainda assim estourar esse limite, se cada uma trouxer volume grande de dados. Bulkificar reduz o número de consultas, mas não substitui filtrar as consultas para trazer só o volume de dados necessário.


DML: uma operação em massa ajuda, mas não torna o volume infinito

Considere:

for (Contact con : contacts) {
    insert con;
}

Cada execução de:

insert con;

representa um novo DML statement.

Com operações suficientes, a transação pode ultrapassar o limite.

No contexto padrão, Salesforce documenta 150 DML statements por transação.

Em vez disso:

insert contacts;

Agora a collection é enviada em uma operação.

Mas atenção:

Uma única operação DML não significa quantidade ilimitada de registros.

Salesforce também controla o número total de registros processados por DML em uma transação, separadamente do número de statements.

DML statements e DML rows são limites diferentes. update accounts; conta como 1 statement, mas se accounts tiver registros demais, você pode esbarrar no limite de linhas mesmo tendo bulkificado corretamente.


CPU Time: quando o problema não está no banco, mas no trabalho que seu código está fazendo

CPU costuma ser o Governor Limit que derruba a ideia simplista de que "basta tirar SOQL de dentro do loop".

Você pode ter pouquíssimas queries e ainda escrever um algoritmo extremamente caro.

Loops aninhados, pesquisas repetidas em listas grandes, cálculos redundantes e processamento desnecessário podem consumir CPU mesmo quando o acesso ao banco está aparentemente bem comportado.

É aqui que Map, Set e escolha de algoritmo deixam de ser apenas organização de código e viram ferramentas de performance.

Nem todo problema de performance envolve SOQL.

Considere:

for (Account acc : accounts) {

    for (Contact con : contacts) {

        if (con.AccountId == acc.Id) {
            // logic
        }
    }
}

Se houver:

1,000 Accounts
5,000 Contacts

o código pode realizar até:

1,000 × 5,000
=
5,000,000 comparisons

Uma estrutura melhor pode indexar os dados:

Map<Id, List<Contact>> contactsByAccount =
    new Map<Id, List<Contact>>();

e acessar diretamente os contatos relacionados.

Governor Limits não significam apenas:

não faça muitas queries

Eles também exigem pensar na eficiência do algoritmo.

CPU Time é um dos limites explicitamente monitorados pela plataforma, e excedê-lo resulta em uma exceção de limite.


Heap Size: quanto desse mundo você está tentando manter na memória?

Se CPU é tempo de trabalho, heap é espaço de memória.

E memória também é recurso finito.

Trazer campos que você não usa, acumular coleções gigantes, manter strings muito grandes ou duplicar estruturas sem necessidade pode aumentar o heap. O objetivo não é desenvolver com medo de cada variável, mas construir consciência de volume.

Quando o dado é grande, a pergunta deixa de ser só "consigo consultar?" e passa a ser "preciso manter tudo isso em memória ao mesmo tempo?".

Heap é a memória ocupada durante a execução Apex por estruturas como:

  • objetos;
  • listas;
  • maps;
  • sets;
  • strings;
  • dados carregados em memória.

Imagine:

List<Contact> contacts = [
    SELECT Id,
           Name,
           Description
    FROM Contact
];

Se sua lógica não precisa de Description, carregá-la apenas aumenta os dados mantidos em memória.

A ideia não é:

Sempre coloque LIMIT 100.

A ideia é:

Busque os dados necessários para cumprir o requisito, com uma estratégia adequada ao volume esperado.

Heap Size é uma das categorias explicitamente controladas pelos Governor Limits Apex.


Outros limites que vale ter no radar

Além de SOQL, DML, CPU e Heap, existem limites em outras frentes que aparecem menos no dia a dia, mas que também derrubam a transação quando estourados.

Callouts

Uma transação pode fazer no máximo 100 chamadas HTTP/webservice externas, com um timeout acumulado total para todos os callouts daquela transação. Isso é separado do CalloutException visto na seção 14: uma coisa é uma chamada individual falhar, outra é a transação tentar fazer chamadas demais.

E-mails

Existe um limite de quantas invocações de método de envio de e-mail (Messaging.sendEmail e afins) uma transação pode fazer, e também um limite diário de endereços externos que o org como um todo pode enviar e-mail.

Future methods e Queueable jobs

  • Existe um teto para quantos métodos @future podem ser invocados dentro de uma única transação.
  • Existe um teto para quantos jobs podem ser enfileirados via System.enqueueJob() dentro de uma transação.
  • Uma regra específica de Queueable: quando você já está dentro de um job Queueable em execução, só é permitido encadear (enqueue) uma quantidade bem mais restrita de próximos jobs: geralmente apenas 1: para evitar cadeias infinitas.

Tamanho de código Apex

Existem limites estruturais que não são "por transação", mas sim sobre o próprio código: tamanho máximo de uma classe Apex, de um trigger, e quantidade total de código Apex permitida no org inteiro. Esses limites não aparecem em runtime: eles bloqueiam o deploy do código quando excedidos, por isso se encaixam mais perto da categoria "compile-time / deploy-time" (seção 2) do que "runtime", mesmo sendo chamados de Governor Limits.

Um ponto de cautela sobre os números

Os valores citados acima e nas seções anteriores variam de fonte para fonte dependendo de quando foram publicados, porque a Salesforce revisa esses números entre releases (principalmente para operações assíncronas). Para qualquer número tratado como "verdade absoluta" num material didático: inclusive este: o ideal é confirmar contra a página oficial Execution Governors and Limits, do Apex Developer Guide, no momento da publicação.


Governor Limit não é algo para "resolver depois" com catch

Aqui eu gosto de ser bem clara: Governor Limit é principalmente um problema de prevenção arquitetural.

Se você chegou ao limite, a plataforma já decidiu que aquela transação ultrapassou uma fronteira que precisava ser protegida. Não é o momento de confiar em um catch genérico como plano de recuperação.

O lugar de resolver esse problema é antes: consultas consolidadas, DML em coleção, estruturas de dados adequadas, divisão consciente de processamento e desenho compatível com o volume.

Essa talvez seja uma das diferenças mais importantes deste artigo, porque muda completamente a estratégia de correção.

Imagine:

try {

    // código executa SOQL demais

} catch (Exception e) {

    System.debug('I handled it');
}

Você não pode utilizar esse padrão como mecanismo de recuperação para System.LimitException.

A Salesforce documenta que Governor Limit exceptions não podem ser tratadas pelo catch convencional. Quando o limite é excedido, elas se comportam como exceptions não tratadas e a transação falha.

Por isso:

NORMAL EXCEPTION
       ↓
pode ser possível
capturar e tratar

enquanto:

GOVERNOR LIMIT
       ↓
precisa principalmente
ser PREVENIDO

Parte 4: O erro aconteceu. E agora, o que acontece com a transação?

Até aqui nós olhamos muito para por que a execução falhou.

Agora vem a pergunta que aparece logo depois em sistemas reais:

"Tá, mas o que ficou salvo?"

Essa pergunta importa muito mais do que parece.

E os dados que já tinham sido alterados antes da falha?

Quando uma falha acontece no meio do caminho, a tentação é olhar só para a linha vermelha. Mas olha um pouco mais longe: pense na transação inteira.

Essa é uma mudança de maturidade importante em Apex.

A pergunta não é somente "o método parou?". Também precisamos saber quais alterações tinham ocorrido, quais ainda faziam parte da mesma unidade transacional e qual comportamento de rollback se aplica.

Esse ponto é muito importante porque uma transação não é apenas uma sequência visual de linhas: ela também define o destino das alterações de dados realizadas naquele contexto.

Imagine:

insert account;

insert contact;

A Account é inserida.

Depois alguma operação gera uma exception não tratada.

Em uma transação Apex normal, uma exception não tratada causa rollback das operações DML realizadas naquela transação. A Salesforce também fornece mecanismos de controle transacional quando é necessário implementar comportamento diferente.

Portanto, não pense apenas:

o método parou

Pense também:

o que aconteceu com as alterações
que já haviam sido realizadas?

Transação é parte do raciocínio sobre erros Apex.


Nem sempre você quer que um registro inválido derrube os outros 99

E aqui entramos numa decisão que não é só técnica. É decisão de negócio.

Se você está processando 100 registros e 1 é inválido, o requisito espera que os outros 99 sejam salvos ou que todos sejam revertidos?

Não existe uma resposta universal.

Existe uma decisão que precisa ser explícita.

A classe Database e os resultados individuais são úteis justamente quando o processo aceita sucesso parcial e você precisa saber exatamente quais registros falharam, sem fingir que nada aconteceu.

Existe ainda outra situação importante, especialmente quando processamos dados em volume.

Considere vários registros sendo processados.

Com DML tradicional:

insert contacts;

uma falha pode fazer a operação falhar conforme o comportamento transacional aplicável.

Mas Apex também fornece métodos da classe Database que permitem trabalhar com resultados individuais e cenários de processamento parcial.

Conceitualmente:

10 registros enviados

8 válidos
2 inválidos

Dependendo da estratégia escolhida, você pode precisar:

ROLLBACK DE TODOS

ou:

8 sucessos
2 erros registrados

Esses são requisitos diferentes.

Por isso, "tratar erros" também envolve decidir se o processo deve ser atômico ou permitir partial success. A Salesforce recomenda o uso da classe Database quando processamento parcial de registros é necessário.


Parte 5: Tratar erro não é fazer o erro desaparecer

Tem uma fase no aprendizado em que try/catch parece quase mágico.

O código quebra, você coloca um catch, a tela para de explodir e vem a sensação de que o problema foi resolvido.

Só que existe uma diferença enorme entre capturar uma exception e tratar uma exception.

Vamos conversar sobre isso.

Por favor, não varra a exception para debaixo do tapete

Sabe aquele catch vazio que faz o erro "sumir"? Ele pode deixar a tela silenciosa e criar um problema muito pior para quem vai investigar depois.

É como varrer cacos de vidro para debaixo do tapete. A sala parece limpa agora, mas o risco continua ali e alguém vai encontrá-lo depois, provavelmente sem saber quem quebrou o vidro nem quando.

Se você decidiu capturar uma exception, precisa existir uma razão para isso. Registrar, transformar, compensar, informar, repropagar ou tomar outra ação coerente. Silenciar não é tratar.

Um antipadrão perigoso é:

try {

    update contacts;

} catch (Exception e) {

}

Agora o erro aconteceu.

Mas o sistema não informa nada.

Não registra nada.

Não propaga nada.

O restante da aplicação pode até acreditar que tudo ocorreu corretamente.

Um catch vazio não é error handling.

É ocultação do erro.

A análise estática fornecida pelas ferramentas Salesforce inclusive identifica blocos try/catch/finally vazios entre padrões problemáticos de código.


Mas também não jogue uma parede de erro técnico na cara de quem usa o sistema

Só que o extremo oposto também machuca a experiência.

Quem usa a aplicação não precisa receber uma parede de stack trace, nomes internos de classes e detalhes de implementação para conseguir entender o que fazer.

Uma boa estratégia tem duas audiências:

  • engenharia, que precisa de contexto técnico;
  • usuário, que precisa de orientação clara.

A mensagem pode ser diferente para cada uma sem perder transparência.

Só que o extremo oposto também machuca a experiência.

Imagine mostrar:

System.NullPointerException
Class.CustomerService.processCustomer: line 172

para um usuário final.

Isso é útil para desenvolvedores.

Não é uma boa mensagem de negócio.

O ideal normalmente é separar:

ERRO TÉCNICO
    ↓
logs / monitoramento / diagnóstico

de:

MENSAGEM PARA O USUÁRIO
    ↓
clara / segura / útil

A documentação Salesforce recomenda controlar os detalhes retornados pelo servidor ao cliente e oferece custom exceptions e AuraHandledException para cenários em que o erro precisa ser apresentado de forma controlada.


Às vezes a exception é só o mensageiro. O bug começou bem antes

Não trate automaticamente toda exception como situação normal.

Considere:

NullPointerException

Você poderia fazer:

try {
    // logic
} catch (NullPointerException e) {
    // ignore
}

Mas talvez o verdadeiro problema seja:

por que esse objeto está null?

A própria ferramenta Salesforce Code Analyzer trata possíveis NullPointerException como situações que devem ser investigadas e recomenda null checks ou inicialização adequada das variáveis.

Um catch não corrige automaticamente a causa.


Parte 6: Como eu investigaria isso numa situação real

Agora vamos juntar as peças.

Imagine que você abriu um chamado, recebeu um log enorme ou viu um teste quebrando minutos antes de um deploy.

Por onde começar sem entrar naquele modo de tentativa e erro?

Eu começaria com cinco perguntas.

Antes de mexer no código, eu faria estas cinco perguntas

Se amanhã você esquecer metade dos nomes deste artigo, tudo bem. Mas eu gostaria que você guardasse esta rotina de investigação.

Quando estamos sob pressão, é muito fácil entrar no modo "vou mexer até funcionar". O problema é que tentativa e erro sem classificação pode até remover o sintoma e deixar a causa intacta.

Estas cinco perguntas funcionam como uma pequena triagem antes de tocar no código.

Quando encontrar um erro Apex, investigue nesta ordem:

1. O código compilou?

Se não:

Compile-time problem

Corrija o código.

2. Qual é o tipo da exception?

Exemplo:

DmlException
QueryException
NullPointerException
CalloutException

O nome já oferece uma pista importante.

3. Qual linha causou o problema?

Stack trace e informações da exception ajudam a identificar a origem.

4. Isso é uma situação esperada ou um bug?

Exemplo:

Nenhuma Account encontrada

pode ser normal.

Mas:

Variável que deveria existir está null

pode indicar defeito na lógica.

5. É uma exception normal ou um Governor Limit?

Se for:

System.LimitException

não pense primeiro:

como capturo?

Pense:

por que minha transação está consumindo
tantos recursos?

Primeira regra de debugging: leia a mensagem inteira

O nome da exception é a categoria. A mensagem é o contexto.

Parar no nome é como receber "problema no motor" e ignorar todo o diagnóstico que vem depois.

Especialmente em DML, query, validações e integrações, o texto restante costuma apontar para o dado, operação ou condição que realmente interessa.

Não leia somente:

System.DmlException

Leia:

System.DmlException:
Insert failed.
First exception on row 0...

Não leia somente:

System.QueryException

Leia o restante da mensagem.

Não leia somente:

System.LimitException

Veja se é:

Too many SOQL queries

ou:

Too many DML statements

ou:

Apex CPU time limit exceeded

O tipo da exception classifica o problema.

A mensagem ajuda a entender a causa específica.


Depois olhe para o stack trace. Ele está contando o caminho

E aqui existe uma nuance valiosa: "onde a exception apareceu" não é sempre igual a "onde a causa começou".

O stack trace reconstrói o caminho de chamadas. Ele mostra como a execução chegou ao ponto da falha.

Você pode encontrar o NullPointerException na linha 45 de uma classe, mas descobrir que a variável ficou null porque uma etapa três métodos antes não produziu o dado esperado.

Por isso stack trace é mapa de percurso, não oráculo.

Imagine:

System.NullPointerException:
Attempt to de-reference a null object

Class.CustomerService.calculate: line 45
Class.CustomerController.save: line 18

Podemos pensar assim:

Exception type
    ↓
O QUE aconteceu?

Exception message
    ↓
QUAL foi o problema?

Stack trace
    ↓
ONDE o caminho de execução passou?

Por isso, logs Apex são tão importantes durante diagnóstico.

Salesforce CLI também fornece comandos específicos para executar Apex, consultar logs e acompanhar debug logs.


O Debug Log ajuda muito. Mas ele não pensa por você

Eu gosto de pensar no Debug Log como uma testemunha muito detalhista. Ele consegue mostrar eventos, valores, consultas, DML, exceptions e o caminho da execução. Mas ele não conhece a intenção do requisito melhor do que você.

Então use o log para responder perguntas concretas:

  • Qual caminho executou?
  • Qual condição foi verdadeira?
  • Qual valor chegou aqui?
  • Quantas queries já tinham sido consumidas?
  • Qual foi a primeira exception relevante?
  • O que ocorreu imediatamente antes do FATAL_ERROR?

Quanto melhor a pergunta, mais útil o log fica.

Um debug log pode mostrar:

SOQL_EXECUTE_BEGIN
DML_BEGIN
EXCEPTION_THROWN
FATAL_ERROR

Ele ajuda você a reconstruir a execução.

Mas encontrar:

NullPointerException

no log não explica automaticamente por que o objeto deveria ter sido preenchido e não foi.

O trabalho do desenvolvedor é combinar:

tipo da exception
+
mensagem
+
stack trace
+
variáveis
+
ordem de execução
+
regra de negócio

até chegar à causa.


Dá para observar os limites antes de bater no teto

Você também não precisa esperar a transação explodir para começar a olhar consumo. A classe Limits transforma esse consumo em informação observável durante a execução.

Mas use-a como instrumento de diagnóstico, não como licença para chegar sempre perto do teto.

Se uma rotina normalmente consome 98 de 100 queries, ela pode até "funcionar" hoje e continuar sendo extremamente frágil para pequenas mudanças de volume ou automações adicionais na mesma transação.

Margem também faz parte de design resiliente.

Você não precisa esperar aparecer:

Too many SOQL queries: 101

para pensar em limites.

Apex possui a classe Limits, que permite consultar consumo de recursos durante a execução.

Conceitualmente:

System.debug(
    Limits.getQueries()
);

System.debug(
    Limits.getLimitQueries()
);

Isso permite comparar:

quanto já consumi

com:

quanto está disponível

Mas isso não substitui arquitetura adequada.

Monitorar que você chegou a 99 queries não transforma 99 queries em um bom design.

A estratégia principal continua sendo escrever código eficiente e bulkificado.


E cuidado: síncrono e assíncrono não têm necessariamente o mesmo orçamento

Aqui mora outro atalho perigoso: decorar um número sem decorar o contexto.

Quando alguém diz "o limite de CPU é X" ou "o heap é Y", a pergunta seguinte deveria ser: em qual contexto de execução?

Síncrono e assíncrono podem ter budgets diferentes, e outros tipos de limites também possuem regras específicas. Por isso, valores numéricos devem ser conferidos na documentação oficial vigente sempre que forem relevantes para uma decisão de arquitetura.

Outro erro comum de iniciante é memorizar:

CPU = X
SOQL = Y
Heap = Z

e imaginar que esses valores valem para qualquer contexto Apex.

Não necessariamente.

Salesforce diferencia limites conforme o contexto de execução. Por exemplo, operações assíncronas podem possuir budgets diferentes de CPU e outros recursos.

Por isso a regra correta é:

Aprenda os principais valores, mas consulte a documentação oficial quando estiver desenhando ou diagnosticando uma solução real.

Isso também protege seu código e seu conhecimento contra mudanças futuras na plataforma.


"Então eu jogo para Batch e resolvo?" Também não

Batch Apex ajuda muito, mas ele não é uma espécie de passe VIP que atravessa Governor Limits.

Ele ajuda a transformar um processamento grande em transações menores. Isso pode ser exatamente o que você precisa para volume elevado.

Mas cada execução ainda precisa ser saudável.

Se o execute faz SOQL dentro de loop, DML registro a registro ou trabalho de CPU desnecessário, o Batch apenas repetirá o problema em pedaços menores.

Outro conceito importante.

Quando existe volume elevado, Batch Apex pode dividir o processamento em diferentes transações.

Isso pode ajudar porque cada execução trabalha em um escopo próprio e recebe seus limites correspondentes.

Mas isso não significa:

Batch Apex = sem limites

Significa:

grande trabalho
      ↓
dividido em transações menores
      ↓
cada transação ainda possui limites

Portanto, código ruim dentro de um Batch continua podendo atingir Governor Limits.


Repara como a pergunta certa muda conforme o problema

Se você chegou até aqui, repara numa coisa: o nome do problema já começa a sugerir qual investigação iniciar.

Isso é fluência.

Não significa decorar todas as classes de exception. Significa reconhecer famílias de problema e saber fazer a primeira pergunta útil.

Quando você vê:

NullPointerException

pergunte:

Qual objeto está null e por quê?

Quando vê:

DmlException

pergunte:

Qual registro ou regra fez a operação DML falhar?

Quando vê:

QueryException

pergunte:

Qual suposição sobre minha consulta ou resultado não foi satisfeita?

Quando vê:

ListException

pergunte:

Minha operação sobre a collection é válida?

Quando vê:

CalloutException

pergunte:

O que aconteceu na chamada externa?

Quando vê:

FinalException

pergunte:

Estou tentando editar um registro fora do ponto certo do fluxo?

Quando vê:

Too many SOQL queries

pergunte:

Por que esta transação está executando tantas consultas?

Quando vê:

Apex CPU time limit exceeded

pergunte:

Qual parte da execução está consumindo processamento demais?

Esse é o começo do debugging.


O mapa mental que eu gostaria de ter ao lado do monitor quando comecei

Se eu pudesse colocar uma folha ao lado do monitor de quem está começando com Apex, seria algo parecido com o mapa abaixo.

Não para substituir documentação, Debug Log ou teste, mas para impedir que toda tela vermelha pareça o mesmo problema.

MEU APEX DEU ERRO
        │
        ▼
O código compilou?
        │
   ┌────┴────┐
   │         │
  NÃO       SIM
   │         │
   ▼         ▼
Compile    O erro aconteceu
error      durante execução
             │
             ▼
      É Governor Limit?
             │
        ┌────┴────┐
        │         │
       SIM       NÃO
        │         │
        ▼         ▼
   PREVENIR      Exception
   OTIMIZAR         │
   BULKIFICAR       ▼
                identificar
                causa
                   │
                   ▼
              pode/deve ser
                tratada?
                   │
              ┌────┴────┐
              │         │
             SIM       NÃO
              │         │
              ▼         ▼
          tratamento   corrigir/
          apropriado   propagar

Se você quiser guardar uma versão curta na cabeça

Exceptions mais comuns

Problema Quando acontece Exemplo try/catch resolve?
Compile-time error Antes da execução Método inexistente Não
NullPointerException Runtime Objeto null utilizado Pode ser capturada, mas a causa deve ser investigada
DmlException Runtime insert ou update falhou Sim
QueryException Runtime Resultado da query incompatível com a operação Sim
ListException Runtime Índice inválido Sim
SObjectException Runtime Uso inválido de SObject Sim
TypeException Runtime Conversão incompatível Sim
StringException Runtime Valor String inválido para operação Sim
MathException Runtime Operação matemática inválida Sim
JSONException Runtime Problema no processamento JSON Sim
CalloutException Runtime Problema durante callout Sim
Custom Exception Runtime Regra definida pela aplicação Sim
System.LimitException Runtime Governor Limit excedido Não pelo catch convencional

Exceptions menos frequentes, mas nativas

Exception Quando acontece
VisualforceException Problema geral em uma página Visualforce
NoAccessException Acesso negado a um sObject/campo (contexto Visualforce)
NoDataFoundException Acesso a um registro que não existe mais (contexto Visualforce)
SerializationException Falha ao serializar dados (contexto Visualforce)
AsyncException Falha ao enfileirar uma operação assíncrona
ExternalObjectException Problema ao acessar dados via External Objects/Salesforce Connect
FinalException Alteração de registro read-only ou variável final
EmailException Falha relacionada ao envio de e-mail
XmlException Falha ao ler/escrever XML via XmlStream
SearchException Problema em SOSL via SOAP API search()
SecurityException Problema nos métodos estáticos da classe Crypto
NoSuchElementException Acesso a elemento além do fim de um Iterator
InvalidParameterValueException Parâmetro inválido passado a um método
RequiredFeatureMissing Recurso do Chatter exigido, mas não habilitado no org

A lista acima não pretende representar todas as exceções que a plataforma pode vir a ter no futuro. Ela reúne o conjunto documentado atualmente, organizado para dar uma base mental sólida a quem está começando.


No fim, tudo volta para a mesma pergunta

Depois de passar por NullPointerException, DmlException, QueryException, CPU, heap, SOQL, DML e todo o resto, parece que estamos falando de dezenas de problemas diferentes.

Mas existe uma pergunta que costura tudo:

O que exatamente aconteceu com esta transação?

O código nem conseguiu começar?

Então olhe para compilação, tipos, referências e estrutura.

Ele começou, mas encontrou um estado inesperado?

Então olhe para os dados, as variáveis, o fluxo, a mensagem da exception e o stack trace.

Ele consumiu recurso demais?

Então não procure uma saída milagrosa em catch. Volte para a arquitetura, para o volume, para as coleções, para as queries, para o DML e para o algoritmo.

É isso que eu gostaria que você levasse deste artigo.

Não uma lista decorada.

Uma forma de pensar.

Porque, na prática, você não precisa reconhecer cada mensagem de memória para ser uma boa pessoa desenvolvedora Apex.

Você precisa saber fazer a próxima pergunta certa.

Na próxima vez que a tela ficar vermelha

Antes de mudar qualquer linha, tenta esta sequência:

1. O código compilou?
2. Qual é o tipo exato da exception?
3. O que a mensagem completa está dizendo?
4. Onde o stack trace aponta?
5. O dado naquela linha era o esperado?
6. Isso é uma situação normal do negócio ou um bug?
7. Existe consumo de Governor Limit envolvido?
8. A correção trata a causa ou só esconde o sintoma?

Se você fizer isso, aquela mensagem que antes parecia apenas "mais um erro do Salesforce" começa a virar um diagnóstico.

E essa mudança é enorme.

Você deixa de brigar com a tela vermelha e começa a conversar com o que a plataforma está tentando te contar.

No começo, ainda dá trabalho.

Depois de um tempo, você olha para:

Too many SOQL queries: 101

e já não pensa:

"Meu Deus, quebrou."

Você pensa:

"Certo. Quem está fazendo query demais nessa transação?"

E é exatamente aí que debugging começa a ficar menos assustador e muito mais interessante.

Fontes oficiais e materiais Salesforce para estudo

Como a plataforma evolui a cada release, principalmente em limites e comportamento de execução, vale manter a documentação oficial como fonte de verdade para números e regras específicas.