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:
- Qual referência está
null? - Ela poderia estar
nullnesse 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
inserte aqueleinsertfalhou; - 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:
- Você tentou modificar um registro em modo somente leitura. O exemplo
clássico é tentar editar
Trigger.newdentro de um triggerafter insertouafter 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
}
}
- 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:
- O comportamento esperado está correto?
- O dado preparado pelo teste está correto?
- O código em produção está retornando algo incorreto?
- 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
@futurepodem 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.
Salesforce Developers: Handle Errors from Apex
https://developer.salesforce.com/docs/platform/lwc/guide/apex-error-handling.html
Referência oficial para built-in exceptions, custom exceptions,try/catch, safe navigation eAuraHandledExceptionem integrações entre Apex e LWC/Aura.Salesforce Developers: Working with Salesforce Records Using SOQL and DML
https://developer.salesforce.com/blogs/2022/08/working-with-salesforce-records-using-soql-and-dml
Explica SOQL, DML, multitenancy, bulkification, rollback e por que Governor Limit exceptions não são um problema resolvido comcatch.Salesforce Developers: Understanding the Bulk of the Salesforce Platform
https://developer.salesforce.com/blogs/engineering/2014/08/understanding-bulk-salesforce1-platform
Material da Salesforce sobre processamento em massa, coleções, SOQL e DML fora de loops.Salesforce Developers: Apex Code Best Practices
https://developer.salesforce.com/ja/wiki/apex_code_best_practices
Reúne práticas de bulkification e demonstra o uso da classeLimitspara observar consumo de Governor Limits.Salesforce Developers: Handle Errors from Apex, LWC Developer Guide
Consulte também os exemplos de error handling e safe navigation vinculados na própria página para ver como o erro percorre backend e frontend.
