There is a situation that almost everyone who starts working with Apex knows.
You are developing, everything looks fine, you run the code, and suddenly:
System.NullPointerException
System.DmlException
System.QueryException
Too many SOQL queries: 101
Apex CPU time limit exceeded
And there it is.
The screen turns red, the test fails, the deployment does not pass, or the Debug Log becomes a huge wall of text.
Then comes that thought:
"Okay. But what exactly did I break?"
If this has happened to you, this article is for that moment.
I do not want you to leave this article memorizing twenty exception names. I want something much more useful: when an error appears, I want you to be able to look at it and think, "Okay, I know where to start."
Because all these messages have one thing in common: they are telling you that something went wrong.
But that is all they have in common.
They are not describing the same kind of problem.
One message may mean your code did not even reach execution. Another may appear because the code started running and found an unexpected situation. And another may mean your transaction asked the platform for more resources than Salesforce allows.
Think about the dashboard of a car.
One light may tell you that a door is open. Another may tell you that fuel is low. Another may warn about a serious engine problem.
They all appear on the same dashboard.
But you would not replace the engine because a door is open, right?
Apex is similar.
The first step is not to put try/catch everywhere.
The first step is to understand what kind of warning you are looking at.
And that is exactly what we are going to do together.
Before we start, keep this map in mind
You will see many names, examples, and situations in this article. If it starts to feel like too much information, come back to this map:
PROBLEM IN APEX
│
├── Compile-time error
│ The code cannot be compiled or validated.
│
├── Runtime exception
│ The code started running, but something went wrong.
│
└── Governor Limit
The transaction used more resources
than the platform allows.
This simple separation already changes the way you investigate a problem.
If the code never started, think about compilation, types, references, and structure.
If the code started and failed during execution, think about exceptions, data, state, and flow.
If the transaction hit a Governor Limit, the question changes completely: what in this transaction is using too many resources?
Let us take it step by step.
In daily work, we call almost everything an "error". And that is fine. The problem starts when this general word makes debugging harder.
To investigate Apex without trying random fixes, it helps to separate the concepts.
Compile-time error
Salesforce analyzes the code and decides that it is not valid for execution.
The program does not start normally.
Runtime exception
The code is valid and starts running, but it finds a situation that prevents the operation from continuing normally.
For example:
String text;
System.debug(text.toUpperCase());
The code is valid from a syntax point of view.
But text contains null.
When Apex tries to execute:
text.toUpperCase()
you can get:
System.NullPointerException
Salesforce documentation uses this kind of situation when explaining
Apex error handling and recommends checking values that can be
null, including the safe navigation operator when
appropriate.
Governor Limit
The code also starts running.
But during the transaction, it consumes more of a specific resource than Salesforce allows.
For example:
System.LimitException:
Too many SOQL queries: 101
Here, the problem is not simply that a query returned an unexpected result.
The transaction executed too many queries.
Governor Limits are part of Salesforce's multitenant architecture. They exist to stop one execution from using too many shared platform resources.
Part 1: Before You Fix Anything, Find Out When the Problem Started
There is one question that solves a big part of the confusion:
Did the code actually start running?
It sounds basic, but this question separates two very different worlds.
If the answer is "no", there is no reason to investigate runtime
variable values, runtime Debug Logs, or catch blocks. If
the answer is "yes", then we start looking at data, state, and the path
of execution.
When the Code Cannot Leave the Garage: Compile-Time Errors
Here is a simple way to think about it: the code is still at the door.
It did not enter execution. It did not query records. It did not run DML. It did not reach a line that could throw a runtime exception.
The platform is basically saying:
"I cannot turn this into executable code yet."
This is why try/catch does not help.
try/catch only exists during execution.
If execution never started, there is no running error-handling block
that can catch anything.
This small detail prevents a very common beginner mistake.
Before a class or trigger can do anything real, the code must pass the first barrier: it must be valid.
Consider:
Integer quantity = 'ten';
quantity is declared as an Integer, but we
are trying to assign a String.
This kind of problem must be fixed before the logic can run.
Another example:
Account acc = new Account();
acc.methodThatDoesNotExist();
That method does not exist for the type.
Another one:
System.debug(customerName);
if customerName was never declared in that scope.
Problems that can appear at this stage include:
- invalid syntax;
- incompatible types;
- a variable that does not exist;
- a method that does not exist;
- an incompatible method signature;
- invalid references to classes, properties, or methods;
- structural problems in the code;
- exceeding the maximum allowed size for a class, trigger, or the total Apex code in the org. This can also block a deployment even when the syntax itself is correct.
The important point for a beginner is:
If there is a compile-time or deployment error, adding
try/catchwill not solve it.
The code must be corrected before it can exist and run in the org.
Part 2: The Code Started Running, and Reality Did Not Follow the Plan
Now we are inside execution.
And here is an important truth about software: your code can be perfectly valid and still meet a world that is different from what you expected when you wrote it.
A record may not exist.
A list may be empty.
An integration may return another format.
A validation rule may block an insert.
A value may be null.
This is the world where many runtime exceptions appear.
The Code Started Running. Then Something Happened
Now the situation changes.
The code passed the previous stage, started running, and found a situation that the logic could not complete normally.
This is where try, catch,
finally, custom exceptions, and error-handling strategies
start to make sense.
But there is one common temptation: you learn that exceptions can be
caught and then think that professional code means putting
try/catch everywhere.
It does not.
Error handling is not a large net that you throw over the whole codebase.
It works better when you can answer three questions:
What failure am I expecting? Why can it happen? What should the system do if it really happens?
Salesforce documents several built-in Apex exceptions and allows you
to catch expected exceptions with try/catch. You can also
create your own custom exceptions.
The basic structure is:
try {
// Code that can throw an exception.
} catch (Exception e) {
// What to do if the exception happens.
}
But this does not mean that all your code should live inside
one giant try/catch block.
Salesforce recommends handling the errors you expect and separating blocks when different parts of the logic need different handling.
This article covers the built-in exceptions you are more likely to see in daily work, and then we will also look at some less common ones that are still part of the official Apex language.
NullPointerException: The Classic "I Thought There Was Something Here"
If there is one exception that almost everyone finds early in Apex, it is this one.
And the name sounds more scary than the idea behind it.
At a basic level, the platform is saying:
"You tried to use something that was not pointing to a valid object at that moment."
The most important part is not adding if (x != null) and
moving on.
The real value comes from asking:
Why did this value reach this point as
null?
Sometimes null is normal.
An optional field can be empty.
A query may not find a relationship.
A parameter can be optional.
In other situations, null shows that one of your
assumptions was wrong.
Maybe your logic expected a variable to be filled in an earlier step, but that step never happened.
So think about NullPointerException with two
questions:
- Which reference is
null? - Could it be
nullhere, or does this show a problem in the flow?
If you work with Apex long enough, this exception becomes familiar.
Almost everyone knows the feeling of seeing
NullPointerException and asking:
"Where did this null come from?"
Consider:
Account acc;
System.debug(acc.Name);
No object was assigned to acc.
So:
acc == null
When the code tries to access:
acc.Name
it tries to use an object that does not exist at that moment.
You may get:
System.NullPointerException:
Attempt to de-reference a null object
How to Think About It
Imagine that a variable is a box.
You wrote:
Account acc;
You created the box.
But you did not put an Account inside it yet.
Then you tried:
acc.Name
It is like asking for the name of the Account that should be inside an empty box.
Prevention
One option is:
if (acc != null) {
System.debug(acc.Name);
}
Apex also supports safe navigation:
String accountName = acc?.Name;
Salesforce recommends null checks, proper variable initialization,
and, when appropriate, safe navigation to avoid dereferencing
null objects.
DmlException: The Code Tried to Save, but Salesforce Said "No"
Now let us move from memory to the database.
A DmlException does not simply mean "Salesforce does not
like my code".
It means a data modification operation was attempted and could not be completed under the rules that applied at that moment.
This difference matters because your code may compile perfectly, and
your insert, update, delete, or
upsert statement may be written correctly.
The problem may be in the data and the rules around that data.
When you see a DmlException, get used to reading the
full message.
It often gives much more useful context than the exception name alone.
DML is what we use when we want to modify records.
For example:
insert account;
update contact;
delete opportunity;
upsert customer;
A DML operation can fail.
A simple example:
Account acc = new Account();
insert acc;
If a data requirement for that operation is not satisfied, the
insert can fail.
Apex may throw:
System.DmlException
Salesforce documents that failed DML operations throw
DmlException. This can happen, for example, when data is
invalid or a validation rule blocks the operation during the
transaction.
You can handle it:
try {
insert acc;
} catch (DmlException e) {
System.debug(e.getMessage());
}
Here Is a Very Common Confusion: DmlException Is Not a DML Governor Limit
This difference deserves a pause.
Imagine two situations:
- you tried an
insert, and thatinsertfailed; - you executed too many DML operations in the same transaction.
Both situations involve DML.
From an architecture point of view, they are completely different problems.
In the first case, we investigate why the operation was not accepted.
In the second case, we investigate why the transaction design consumed too many DML statements.
This separation is one of the main ideas of this article.
Consider:
insert accounts;
You can execute only one DML statement and still get:
System.DmlException
because something failed during that operation.
That is different from:
System.LimitException:
Too many DML statements: 151
Here, the problem is that the transaction executed too many DML statements.
Look at the difference:
| Situation | Meaning |
|---|---|
DmlException |
A DML operation failed |
Too many DML statements |
The transaction exceeded the DML Governor Limit |
This difference is fundamental.
When it becomes clear, you stop trying to fix resource-consumption problems with exception-handling tools.
DML exception is about whether the operation succeeded or failed.
DML Governor Limit is about how many resources the transaction used.
Salesforce documents a limit of 150 DML statements per transaction in the standard context discussed in bulkification guidance.
QueryException: What If the Query Does Not Return What You Expected?
SOQL is another place where a small assumption can quickly become an exception.
The database does not have to return exactly the situation you imagined when you wrote the code.
Real systems have missing records, duplicate data, old records, delayed integrations, and business paths that were not always planned from the beginning.
So one useful question when you see a QueryException
is:
Is the problem in the query, or in the expectation my code created about the query result?
That change of perspective helps a lot.
Consider:
Account acc = [
SELECT Id, Name
FROM Account
WHERE Name = 'Example'
];
Notice something important.
The result is assigned directly to:
Account acc
That means the code expects a result that is compatible with that assignment.
If no record is returned, you may see:
System.QueryException:
List has no rows for assignment to SObject
There are other situations that can also produce
QueryException.
Salesforce explicitly lists QueryException among the
Apex exceptions a developer may need to handle when working with
queries.
If zero records is a normal business possibility, a more resilient option is to work with a collection:
List<Account> accounts = [
SELECT Id, Name
FROM Account
WHERE Name = 'Example'
];
if (!accounts.isEmpty()) {
Account acc = accounts[0];
}
Now zero records is not automatically an exception.
It can simply be an expected query result.
This shows an important principle:
Not every problem needs a
catch. Many exceptions are better prevented by designing the code correctly.
SObjectException: The Record Exists, but You Asked for Something That Was Not Available
This is an interesting case: the object exists, but that does not mean everything about it is available in the way your code expects.
In Apex, especially with SOQL results, we need to remember that we load the fields we request.
If your logic tries to use a field that was not included in the query, the problem is not "the record does not exist".
The problem is how the SObject was prepared and used.
Consider:
Account acc = [
SELECT Id
FROM Account
LIMIT 1
];
System.debug(acc.Name);
The query selected:
Id
but it did not select:
Name
Later, the code tries to access Name.
This can produce:
System.SObjectException
with a message that tells you the requested field was not queried.
The beginner lesson is simple:
When Apex returns an SObject from SOQL, query the fields your logic really needs.
That also does not mean you should always do something like:
SELECT all_fields
Loading unnecessary fields increases the amount of data in memory.
The goal is to query the fields needed for the use case.
ListException: The List Exists. The Element You Want May Not
Notice how this is different from null.
And here is a small difference that can save you a lot of debugging time:
an empty list is still a perfectly valid list.
It exists.
You can inspect it.
And it can correctly represent a business result such as "no records were found".
The error happens when the code treats "the list exists" as if it automatically means "the list contains at least one element".
Consider:
List<Account> accounts = new List<Account>();
Account firstAccount = accounts[0];
The list exists.
But it is empty.
So there is no element at position 0.
The code can throw:
System.ListException:
List index out of bounds: 0
A simple prevention:
if (!accounts.isEmpty()) {
Account firstAccount = accounts[0];
}
The difference from NullPointerException is
important.
Here:
accounts
is not null.
The list exists.
The problem is that the code is trying to access a position that does not exist.
TypeException: When the Value Is Not the Type You Were Sure It Was
When data moves through several layers, the feeling that "I know what type this value is" can be dangerous.
This happens often with Object, dynamic payloads,
deserialization, generic maps, and integrations.
So before forcing a cast, check what actually arrived.
In other words:
Do not work with the type you wish you had. Investigate the type you really have.
Consider:
Object value = 'hello';
Integer numberValue = (Integer) value;
The object contains a String.
We are trying to treat it as an Integer.
This incompatible operation may result in:
System.TypeException
You can find similar situations when working with:
- generic objects;
- casts;
- dynamic data;
- deserialization;
- structures coming from other parts of the application.
A useful question is:
What is the real type of the value that arrived here?
StringException: Not Every String Can Become Everything
A common example involves Salesforce IDs.
Id accountId = 'this-is-not-an-id';
The string is not a valid Salesforce ID.
The operation can throw:
System.StringException:
Invalid id
The code received a String.
But that does not mean every String can be converted to
every other type.
MathException: Yes, Even a Simple Calculation Can Fail at Runtime
Consider:
Integer total = 10;
Integer divisor = 0;
Integer result = total / divisor;
Division by zero is not a valid mathematical operation.
Apex can throw:
System.MathException:
Divide by 0
This shows again the difference between compilation and runtime.
The code:
total / divisor
is valid from a syntax point of view.
The problem appears only when:
divisor == 0
during execution.
JSONException: When the Payload Arrives Different from the Agreement
JSON coming from outside your application deserves a little extra healthy suspicion.
Not because the other system is necessarily "wrong", but because
integration contracts change, fields can become optional, error
responses can have a different format from success responses, and values
can arrive as null when you did not expect them.
A resilient integration treats the payload as input that must be validated, not as a promise of perfection.
Integrations often work with JSON.
For example:
CustomerResponse response =
(CustomerResponse) JSON.deserialize(
jsonBody,
CustomerResponse.class
);
If the received data cannot be processed as expected, serialization or deserialization operations may throw JSON-related exceptions.
A good principle for beginners is:
External data should not be considered automatically valid.
An integration should consider:
- the received structure;
- optional fields;
- data types;
nullvalues;- invalid payloads;
- unexpected responses.
CalloutException: Now There Is Another System in the Conversation
A callout adds something your code does not fully control: another system.
You can write correct code and still face timeouts, service unavailability, wrong endpoint configuration, authentication problems, or unexpected responses.
This is why callout handling needs to separate two ideas:
- the failure of one specific call;
- the transaction's callout limits.
Again, an exception and a Governor Limit can appear around the same subject without having the same cause.
Apex can make HTTP calls to external systems.
For example:
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint('callout:ExampleService/resource');
request.setMethod('GET');
HttpResponse response = http.send(request);
Problems during callout execution may result in:
System.CalloutException
Callouts also have their own platform rules and limits, so it is important to separate these two concepts:
CalloutException
is not automatically the same thing as:
Callout Governor Limit
A single call can fail even when the maximum number of callouts has not been reached.
What About the Exceptions You Almost Never See?
The exceptions above cover many situations you are likely to find in normal development.
Salesforce documentation also includes other built-in exceptions that appear in more specific contexts.
You do not need to memorize them now.
But it helps to recognize the name when one appears instead of feeling completely lost.
I grouped them by "family" because this makes them easier to remember.
Family 1: Visualforce-Related Exceptions
If you work only with LWC, you may never see these.
But if your org still has Visualforce pages, and many orgs do, they can appear.
VisualforceException: a general problem
related to a Visualforce page, for example an issue with rendering or
component binding.
NoAccessException: you tried to access
an sObject or field through Visualforce that the current user is not
allowed to access.
// In a Visualforce controller extension
Account acc = [SELECT Id, Name FROM Account LIMIT 1];
// If the user does not have the required access,
// the operation can throw NoAccessException
NoDataFoundException: you tried to
access a record that no longer exists, for example when it was deleted
between page load and the user's next action.
SerializationException: a problem while
serializing data, commonly in Visualforce contexts when data cannot be
converted to the format expected by the page.
Family 2: Infrastructure and Asynchronous Execution
AsyncException: something went wrong
while trying to queue asynchronous work, for example a failed
System.enqueueJob() call.
try {
System.enqueueJob(new MyQueueable());
} catch (AsyncException e) {
System.debug('Failed to enqueue the job: ' + e.getMessage());
}
ExternalObjectException: a problem
while accessing data stored in external systems through External Objects
and Salesforce Connect, for example a timeout when connecting to the
external system.
FinalException: two common situations
can cause this.
- You tried to modify a record that is read-only. A classic example is
changing
Trigger.newinside anafter insertorafter updatetrigger.
trigger AccountTrigger on Account (after update) {
for (Account acc : Trigger.new) {
acc.Name = 'New Name'; // FinalException:
// Record is read-only
}
}
- You tried to assign a new value to a variable declared as
final.
Prevention: if you need to change the record in the
same flow, do it in before insert or
before update, where the record is still editable, or query
the record again before updating it when that is appropriate.
Family 3: Communication and Data
EmailException: a problem related to
email, for example a failure involving email sent through
Messaging.sendEmail().
XmlException: a problem reading or
writing XML with XmlStreamReader or
XmlStreamWriter.
SearchException: a problem in a SOSL
search executed through the SOAP API search() call, for
example when the search parameter is invalid.
SecurityException: a problem related to
static methods in the Crypto utility class, used for
encryption, digital signatures, and hashing.
Family 4: "This Should Not Have Happened"
NoSuchElementException: you tried to
access an element from an Iterator after the available
elements were finished.
The classic pattern is calling .next() when
.hasNext() has already returned false.
Iterator<Account> it = accounts.iterator();
while (it.hasNext()) {
Account acc = it.next();
}
// Calling it.next() again here could throw NoSuchElementException
InvalidParameterValueException: an
invalid parameter was passed to a method.
RequiredFeatureMissing: the code
requires a feature that is not enabled in the target org, such as a
Chatter-related feature in a deployment scenario.
A Note About Completeness
Even with all the exceptions in this article, this list should not be treated as an eternal picture of 100% of the Apex exception universe.
The platform evolves and can introduce new exceptions in future releases.
When you are not sure, confirm against the official Built-In Exceptions and Common Methods section of the Apex Developer Guide.
AssertException: The Test Is Red. That Does Not Automatically Mean the Test Is Wrong
I know a red test can be frustrating, especially when you only want to finish a deployment.
But the test is not your enemy.
It is trying to tell you a story.
An AssertException means there is a difference between
the behavior the test expected and the behavior that actually
happened.
That is all.
The next step is to investigate which side of that comparison is wrong.
Maybe the production code is wrong.
Maybe the test data is wrong.
Maybe the business rule changed and an old assertion was never updated.
Maybe the expected result was not correct from the beginning.
The worst reaction is changing the assertion only until the test becomes green.
In Apex tests, we make statements about the expected behavior of the code.
For example:
System.assertEquals(
10,
result
);
If result is 8, the test should fail.
That is useful.
A test exists exactly to show when:
actual result != expected result
An assertion failure does not automatically mean the test itself is wrong.
It means you need to investigate:
- Is the expected behavior correct?
- Is the test data correct?
- Is the production code returning the wrong result?
- Is the assertion testing the right business rule?
Never change an assertion only to turn a red test into a green one without understanding the reason for the difference.
Custom Exceptions: When Your Business Needs a Name for the Problem
Not every application problem fits well into:
DmlException
QueryException
NullPointerException
Imagine a business rule:
This customer cannot continue because they do not meet
the criteria for this process.
You can define your own exception:
public class CustomerNotEligibleException extends Exception {
}
And throw it when needed:
if (!isEligible) {
throw new CustomerNotEligibleException(
'Customer is not eligible for this process.'
);
}
Salesforce allows custom exceptions by extending
Exception.
This helps represent application-specific situations more clearly.
AuraHandledException: The User Does Not Need Your Stack Trace
Now think about the person using the screen.
They do not need to speak the same language as your backend.
The development team needs technical details for debugging.
The user needs to know what happened in useful terms so they can continue their work.
This is where good error handling becomes more human: keep the technical details available for the engineering team, while giving the user a controlled and understandable message.
That is not hiding the problem.
It is separating technical observability from user experience.
Imagine an Apex method called by a Lightning Web Component.
You do not necessarily want to send details like this to the end user:
Class.CustomerController line 87
System.NullPointerException...
In some situations, you can return a controlled message:
throw new AuraHandledException(
'We could not complete the operation.'
);
Even with the name AuraHandledException, it is also
supported by Lightning Web Components.
Salesforce explains that it can return a custom message to the client without exposing the stack trace in the same way as an unhandled exception.
This does not mean hiding technical problems.
The system can log technical details for diagnosis while showing the user a message that makes sense in the business context.
Handled or Unhandled: What Happens After the Exception Appears?
Now we can make another useful separation.
Handled Exception
Your code expected that the situation could happen and defined a response.
try {
insert accounts;
} catch (DmlException e) {
System.debug(
'The records could not be inserted: ' +
e.getMessage()
);
}
Unhandled Exception
No code catches the exception.
It continues to propagate until it interrupts the execution.
When Apex is called by a Lightning component, an unhandled exception may reach the client with error information.
Salesforce recommends controlling which errors should be handled on the server and what information should be sent to the client.
"So I Put a Generic Catch Around Everything and I Am Done?" Not Exactly
I understand the temptation.
You learn that catch (Exception e) catches many
exceptions and think:
"Great. Problem solved."
Not really.
A system that "catches everything" can end up understanding nothing.
Good handling does not exist only to stop a red error message.
It exists to make a reasonable decision when a known failure happens.
When the catch is too generic, you can treat very
different situations as if they were the same, lose useful information,
and hide bugs that should reach a layer that can actually handle
them.
Technically, a generic catch can capture many common
exceptions:
try {
// logic
} catch (Exception e) {
// handling
}
But that does not automatically make the code robust.
Compare:
try {
insert accounts;
} catch (DmlException e) {
// specific handling for DML failure
}
with:
try {
insert accounts;
} catch (Exception e) {
// anything
}
In the first case, the code clearly shows the expected failure.
Salesforce recommends handling exceptions that are relevant to the code being executed and handling only errors that you actually expect there, while allowing other errors to propagate when appropriate.
And there is another reason why catch (Exception e) does
not solve everything.
Governor Limit exceptions are not handled this way.
Part 3: When the Problem Is Not Unexpected Data, but How Much the Transaction Is Using
Until now, our conversation was mostly about situations where something unexpected happened during execution.
Governor Limits tell a different story.
Here, Salesforce is not saying that one specific record is wrong.
It is saying that the transaction crossed a resource boundary.
That difference changes what you do next.
Now We Reach Governor Limits, and Here the Logic Changes
This is where the game really changes.
Until now, we were mainly talking about code that finds exceptional situations during execution.
Governor Limits have another purpose: they protect shared platform resources.
Think about a building where many companies share infrastructure.
It would not be sustainable if one office could use all the electricity, all the water, and all the elevators without any limits.
Salesforce follows a similar general idea in a multitenant architecture: one execution cannot use an unlimited amount of shared resources.
That is why the right question is not:
"How do I catch this limit after it happens?"
The better question is:
"How do I design this transaction so it does not reach that point?"
A:
DmlException
can mean one operation failed.
A:
NullPointerException
can mean code tried to use a null reference.
But:
Too many SOQL queries: 101
means the transaction crossed a platform resource limit.
Governor Limits exist because Salesforce runs workloads in a multitenant architecture and must protect shared resources between different customers.
Which Governor Limits Do You Really Need to Know at the Beginning?
Before this starts to look like a huge table you need to memorize: you do not need to know every Salesforce limit on day one.
Start by recognizing the main groups.
Ask if your transaction is using too much in queries, returned rows, DML, DML rows, CPU, heap, or callouts.
Then, when you are building or debugging a real solution, confirm the current value in the official documentation for that execution context.
This is better than treating numbers like permanent rules, especially because some limits can be different in synchronous and asynchronous contexts and may change between releases.
Salesforce has several categories of limits, including:
- Per-transaction limits: these start again for each transaction, and in Batch Apex each execution chunk gets its own transaction context.
- Structural/code-size limits: these are not per transaction. They limit code size and can block deployment.
- Platform limits: these are broader platform rules and are not always tied to one Apex transaction.
The first limits I would learn are:
| Limit | Category | What it controls |
|---|---|---|
| SOQL Queries | Transaction | How many queries run |
| SOQL Query Rows | Transaction | Total rows returned by queries |
| DML Statements | Transaction | How many DML operations run |
| DML Rows | Transaction | Total records processed by DML |
| CPU Time | Transaction | How much CPU time the transaction uses |
| Heap Size | Transaction | How much Apex memory is used |
| Callouts | Transaction | How many external calls are made |
| Email invocations | Transaction | How many email-send method calls are made |
| Future Methods | Transaction | How many @future calls are made |
| Queueable Jobs | Transaction | How many jobs are added with System.enqueueJob() |
| Apex code size | Structural | Maximum class, trigger, and org Apex code size |
Salesforce applies different categories of limits, including CPU, heap, and transaction limits.
Some values also depend on the execution context, especially synchronous versus asynchronous execution.
SOQL: It Is Not Only About How Many Queries You Run
Let us separate two questions that beginners often mix:
- How many queries did I execute?
- How many records did those queries return?
You can run a small number of queries and still return too much data.
You can also return only a few records per query and still run too many queries.
Bulkification helps a lot with the first problem, but it does not replace good filters and a data-volume strategy for the second problem.
A classic example of exceeding the number of queries is:
for (Account acc : accounts) {
List<Contact> contacts = [
SELECT Id
FROM Contact
WHERE AccountId = :acc.Id
];
}
If the loop runs enough queries, you can get:
System.LimitException:
Too many SOQL queries: 101
In the common synchronous context, Salesforce documents a maximum of 100 SOQL queries per transaction.
The solution is not:
try {
// queries
} catch (Exception e) {
// save me
}
The solution is to design the query correctly.
For example:
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
];
Now one query works with the whole set of records.
This principle is called bulkification.
Salesforce recommends using collections and avoiding SOQL and DML inside loops as a key practice for respecting Governor Limits.
But there is another limit, separate from the number of queries: the total number of rows returned by all queries in the transaction.
In common Apex contexts, this can be around 50,000 query rows.
So you can run only a few queries and still hit a limit if those queries return too much data.
Bulkification reduces the number of queries, but it does not replace filtering your queries so they return only the data you need.
DML: One Bulk Operation Helps, but It Does Not Make Volume Infinite
Consider:
for (Contact con : contacts) {
insert con;
}
Every time this line runs:
insert con;
it is another DML statement.
With enough iterations, the transaction can exceed the limit.
In the standard context, Salesforce documents 150 DML statements per transaction.
Instead:
insert contacts;
Now the collection is sent in one DML statement.
But be careful:
One DML statement does not mean an unlimited number of records.
Salesforce also controls the total number of records processed by DML in one transaction.
DML statements and DML rows are different limits.
update accounts; counts as one statement.
But if accounts contains too many records, you can still
hit the DML rows limit even though the code is correctly bulkified.
Keep this pair in mind:
statement tells you how many DML operations you started.
rows tells you how many records those operations processed.
CPU Time: When the Problem Is Not the Database, but the Work Your Code Is Doing
CPU Time is one of the Governor Limits that breaks the simple idea that "I only need to remove SOQL from loops".
You can have very few queries and still write a very expensive algorithm.
Nested loops, repeated searches through large lists, repeated calculations, and unnecessary processing can use a lot of CPU even when database access looks fine.
This is where Map, Set, and algorithm
design stop being only code-organization tools and become performance
tools.
Not every performance problem involves SOQL.
Consider:
for (Account acc : accounts) {
for (Contact con : contacts) {
if (con.AccountId == acc.Id) {
// logic
}
}
}
If you have:
1,000 Accounts
5,000 Contacts
the code may perform up to:
1,000 × 5,000
=
5,000,000 comparisons
A better structure can index the data:
Map<Id, List<Contact>> contactsByAccount =
new Map<Id, List<Contact>>();
and then access related Contacts directly.
Governor Limits are not only saying:
do not run too many queries
They also force us to think about algorithm efficiency.
CPU Time is one of the resources Salesforce explicitly monitors, and exceeding it produces a limit exception.
Heap Size: How Much of the World Are You Trying to Keep in Memory?
If CPU is work time, heap is memory space.
And memory is also limited.
Selecting fields you do not use, keeping huge collections, storing very large strings, or creating duplicate structures without a good reason can increase heap usage.
The goal is not to become afraid of every variable.
The goal is to understand data volume.
When the data is large, the question is no longer only:
"Can I query this?"
It also becomes:
"Do I need to keep all this data in memory at the same time?"
Heap is the memory used during Apex execution by structures such as:
- objects;
- lists;
- maps;
- sets;
- strings;
- data loaded into memory.
Imagine:
List<Contact> contacts = [
SELECT Id,
Name,
Description
FROM Contact
];
If your logic does not need Description, selecting it
only increases the data you keep in memory.
The idea is not:
Always add
LIMIT 100.
The better idea is:
Query the data you need for the requirement, with a strategy that fits the expected volume.
Heap Size is one of the resource categories controlled by Apex Governor Limits.
Other Limits Worth Keeping on Your Radar
Besides SOQL, DML, CPU, and Heap, other limits can appear less often but can still stop a transaction when exceeded.
Callouts
A transaction can make a limited number of HTTP or web service calls.
This is separate from CalloutException.
One external call can fail because of a timeout or authentication issue even when the transaction is still far below the callout count limit.
Emails
There are limits for email-send method invocations in a transaction, and there are also broader daily email limits for the org.
Future Methods and Queueable Jobs
- There is a maximum number of
@futuremethod calls that can be started from one transaction. - There is a maximum number of Queueable jobs that can be added with
System.enqueueJob()in one transaction. - There are also more restrictive rules when you are already inside an executing Queueable job and want to chain another job.
Apex Code Size
Some limits are structural rather than per transaction.
Examples include maximum class size, trigger size, and total Apex code allowed in an org.
These do not usually appear as runtime failures.
They can block deployment instead.
So even though they are part of platform limits, they are closer to compile-time or deployment-time problems than normal runtime exceptions.
A Caution About Numbers
Specific limit values can change by execution context and by Salesforce release.
Because of that, any number presented as an absolute truth in training material, including this article, should be checked against the current Execution Governors and Limits documentation before publication or architecture decisions.
A Governor Limit Is Not Something You "Fix Later" with Catch
Here I want to be very clear:
Governor Limits are mainly an architecture and prevention problem.
If you reached the limit, the platform already decided that the transaction crossed a protected resource boundary.
This is not the moment to trust a generic catch as a
recovery plan.
The real place to solve the problem is earlier:
- consolidated queries;
- DML on collections;
- good data structures;
- intentional division of processing;
- algorithms designed for the expected volume.
This is one of the most important differences in the article because it changes the entire correction strategy.
Imagine:
try {
// code executes too many SOQL queries
} catch (Exception e) {
System.debug('I handled it');
}
You cannot use this pattern as a recovery mechanism for
System.LimitException.
Salesforce documents that Governor Limit exceptions cannot be handled
by a normal catch.
When the limit is exceeded, the transaction fails.
So:
NORMAL EXCEPTION
↓
may be possible
to catch and handle
while:
GOVERNOR LIMIT
↓
must mainly
be PREVENTED
Part 4: The Error Happened. What Happens to the Transaction Now?
Until now, we have mostly looked at why execution failed.
Now comes the question that appears right after that in real systems:
"Okay, but what was actually saved?"
This question matters much more than it seems.
What About Data That Was Already Changed Before the Failure?
When something fails in the middle of the process, the temptation is to look only at the red line.
But look a little wider.
Think about the whole transaction.
This is an important step in Apex maturity.
The question is not only:
"Did the method stop?"
We also need to understand which changes had already happened, which changes were still part of the same transaction, and what rollback behavior applies.
Imagine:
insert account;
insert contact;
The Account is inserted.
Then another operation throws an unhandled exception.
In a normal Apex transaction, an unhandled exception causes DML operations in that transaction to roll back.
Salesforce also provides transaction-control mechanisms when a different behavior is required.
So do not think only:
the method stopped
Also think:
what happened to the changes
that had already been made?
Transaction behavior is part of Apex error reasoning.
Sometimes You Do Not Want One Invalid Record to Stop the Other 99
Now we enter a decision that is not only technical.
It is a business decision.
If you process 100 records and 1 is invalid, should the other 99 be saved?
Or should all 100 be rolled back?
There is no universal answer.
The requirement needs to be explicit.
The Database class and its individual result objects are
useful when partial success is accepted and you need to know exactly
which records failed without pretending everything was fine.
Consider several records being processed.
With traditional DML:
insert contacts;
a failure can cause the operation to fail according to the transaction behavior that applies.
But Apex also provides Database methods that allow
individual results and partial-processing scenarios.
Conceptually:
10 records sent
8 valid
2 invalid
Depending on the business rule, you may need:
ROLL BACK ALL
or:
8 successes
2 recorded errors
These are different requirements.
This is why "error handling" also includes deciding whether the process must be atomic or whether partial success is allowed.
Salesforce recommends using the Database class when
partial record processing is required.
Part 5: Handling an Error Is Not the Same as Making the Error Disappear
There is a moment in learning Apex when try/catch feels
almost magical.
The code breaks.
You add a catch.
The red message disappears.
And it feels like the problem is solved.
But there is a big difference between catching an exception and handling an exception.
Let us talk about that.
Please Do Not Sweep the Exception Under the Rug
You know that empty catch block that makes the error "go
away"?
It may keep the screen quiet and create a much worse problem for the person who needs to debug the system later.
It is like sweeping broken glass under a carpet.
The room looks clean now.
But the danger is still there, and someone will find it later without knowing who broke the glass or when it happened.
If you decide to catch an exception, there should be a reason.
You may want to:
- log it;
- transform it;
- compensate for it;
- inform the user;
- rethrow it;
- take another meaningful action.
Silence is not handling.
A dangerous anti-pattern is:
try {
update contacts;
} catch (Exception e) {
}
The error happened.
But the system says nothing.
It logs nothing.
It throws nothing.
The rest of the application may even continue as if everything worked.
An empty catch is not error handling.
It is error hiding.
Salesforce static analysis tools also identify empty
try/catch/finally blocks as problematic code patterns.
But Do Not Throw a Wall of Technical Error Text at the User Either
The opposite extreme is also bad for the experience.
A user does not need a huge stack trace, internal class names, and implementation details to understand what to do next.
A good strategy has two audiences:
- engineering, which needs technical context;
- the user, who needs clear guidance.
The message can be different for each audience without losing transparency.
Imagine showing this:
System.NullPointerException
Class.CustomerService.processCustomer: line 172
to an end user.
That is useful to a developer.
It is not a good business message.
A better design normally separates:
TECHNICAL ERROR
↓
logs / monitoring / diagnosis
from:
USER MESSAGE
↓
clear / safe / useful
Salesforce recommends controlling the details returned from the
server to the client and provides custom exceptions and
AuraHandledException for situations where an error needs to
be shown in a controlled way.
Sometimes the Exception Is Only the Messenger. The Bug Started Earlier
Do not automatically treat every exception as a normal expected situation.
Consider:
NullPointerException
You could do:
try {
// logic
} catch (NullPointerException e) {
// ignore
}
But maybe the real question is:
why is this object null?
Salesforce Code Analyzer treats possible
NullPointerException scenarios as situations that should be
investigated and recommends null checks or correct variable
initialization.
A catch does not automatically fix the cause.
Part 6: How I Would Investigate This in a Real Situation
Now let us connect everything.
Imagine you received a support ticket, a huge log, or a failing test a few minutes before a deployment.
Where do you start without entering random trial-and-error mode?
I would start with five questions.
Before Changing the Code, I Would Ask These Five Questions
If tomorrow you forget half of the exception names in this article, that is okay.
But I would like you to remember this investigation routine.
When we are under pressure, it is easy to enter the mode of:
"I will change things until it works."
The problem is that trial and error without classification can remove the symptom and leave the real cause untouched.
These five questions work as a small technical triage before you change the code.
1. Did the Code Compile?
If not:
Compile-time problem
Fix the code first.
2. What Is the Exception Type?
For example:
DmlException
QueryException
NullPointerException
CalloutException
The name already gives you an important clue.
3. Which Line Caused the Problem?
The stack trace and exception details help identify where the failure appeared.
4. Is This an Expected Situation or a Bug?
For example:
No Account found
may be normal.
But:
A variable that should exist is null
may show a problem in the logic.
5. Is This a Normal Exception or a Governor Limit?
If you see:
System.LimitException
do not think first:
how do I catch it?
Think:
why is my transaction using
so many resources?
First Debugging Rule: Read the Whole Message
The exception name is the category.
The message is the context.
Stopping at the name is like hearing "engine problem" and ignoring the rest of the diagnosis.
Especially with DML, queries, validations, and integrations, the rest of the text often points to the data, operation, or condition that actually matters.
Do not read only:
System.DmlException
Read:
System.DmlException:
Insert failed.
First exception on row 0...
Do not read only:
System.QueryException
Read the rest of the message.
Do not read only:
System.LimitException
Check if it is:
Too many SOQL queries
or:
Too many DML statements
or:
Apex CPU time limit exceeded
The exception type classifies the problem.
The message helps explain the specific cause.
Then Look at the Stack Trace. It Is Showing the Path
Here is an important detail:
"Where the exception appeared" is not always the same as "where the cause started".
The stack trace reconstructs the call path.
It shows how execution reached the point of failure.
You may find a NullPointerException on line 45 of a
class and later discover that the variable became null
because a step three methods earlier did not produce the expected
data.
That is why a stack trace is a route map, not an oracle.
Imagine:
System.NullPointerException:
Attempt to de-reference a null object
Class.CustomerService.calculate: line 45
Class.CustomerController.save: line 18
You can think about it like this:
Exception type
↓
WHAT happened?
Exception message
↓
WHAT was the specific problem?
Stack trace
↓
WHERE did execution travel?
This is why Apex logs are so important during diagnosis.
Salesforce CLI also provides commands to execute Apex, retrieve logs, and work with debug logs.
The Debug Log Helps a Lot. But It Does Not Think for You
I like to think about the Debug Log as a very detailed witness.
It can show events, values, queries, DML, exceptions, and the execution path.
But it does not know the business intention better than you do.
So use the log to answer specific questions:
- Which path executed?
- Which condition was true?
- Which value reached this point?
- How many queries had already been used?
- What was the first relevant exception?
- What happened immediately before
FATAL_ERROR?
The better your question, the more useful the log becomes.
A Debug Log can show:
SOQL_EXECUTE_BEGIN
DML_BEGIN
EXCEPTION_THROWN
FATAL_ERROR
It helps you rebuild the execution.
But finding:
NullPointerException
in the log does not automatically explain why the object should have been filled but was not.
Your real job is to connect:
exception type
+
message
+
stack trace
+
variables
+
execution order
+
business rule
until you reach the cause.
You Can Watch Governor Limits Before You Hit the Ceiling
You do not need to wait for the transaction to fail before looking at resource usage.
The Limits class makes resource consumption visible
during execution.
For example:
System.debug(
Limits.getQueries()
);
System.debug(
Limits.getLimitQueries()
);
This lets you compare:
how much I already used
with:
how much is available
But this does not replace good architecture.
If a routine normally uses 98 of 100 queries, it may technically "work" today and still be very fragile.
A small increase in data volume or another automation in the same transaction can push it over the limit.
Good design also needs margin.
The main strategy is still efficient and bulkified code.
Be Careful: Synchronous and Asynchronous Apex Do Not Always Have the Same Budget
Another dangerous shortcut is memorizing a number without memorizing the context.
When someone says:
"CPU limit is X"
or:
"Heap is Y"
the next question should be:
In which execution context?
Synchronous and asynchronous operations can have different resource budgets.
Other limits also have context-specific rules.
So specific numeric values should be confirmed in the current official documentation whenever they matter to an architecture decision or production diagnosis.
The correct rule is:
Learn the main limits, but confirm the official documentation when designing or debugging a real solution.
This also protects your knowledge from future platform changes.
"So I Move It to Batch and the Problem Is Solved?" Also No
Batch Apex helps a lot, but it is not a VIP pass through Governor Limits.
It helps divide a large amount of work into smaller transactions.
That can be exactly what you need when processing large volumes.
But each execution still needs to be healthy.
If the execute method does SOQL in loops, DML record by
record, or unnecessary CPU-heavy processing, Batch will only repeat the
bad design in smaller pieces.
Conceptually:
large amount of work
↓
split into smaller transactions
↓
each transaction still has limits
So:
Batch Apex = no limits
is not true.
Bad code inside a Batch job can still hit Governor Limits.
Notice How the Right Question Changes with the Problem
If you reached this point, notice something important:
the name of the problem is already starting to suggest what kind of investigation to begin.
That is fluency.
It does not mean memorizing every exception class.
It means recognizing families of problems and knowing the first useful question to ask.
When you see:
NullPointerException
ask:
Which object is
null, and why?
When you see:
DmlException
ask:
Which record or rule caused the DML operation to fail?
When you see:
QueryException
ask:
Which assumption about my query or result was not satisfied?
When you see:
ListException
ask:
Is my operation on the collection valid?
When you see:
CalloutException
ask:
What happened in the external call?
When you see:
FinalException
ask:
Am I trying to edit a record at the wrong point in the flow?
When you see:
Too many SOQL queries
ask:
Why is this transaction running so many queries?
When you see:
Apex CPU time limit exceeded
ask:
Which part of the execution is using too much processing time?
That is the beginning of debugging.
The Mental Map I Wish I Had Next to My Monitor When I Started
If I could put one page next to the monitor of someone starting with Apex, it would look something like this.
Not to replace documentation, Debug Logs, or tests.
Only to stop every red screen from looking like the same problem.
MY APEX FAILED
│
▼
Did the code compile?
│
┌──┴──┐
│ │
NO YES
│ │
▼ ▼
Compile Error happened
error during execution
│
▼
Is it a Governor Limit?
│
┌────┴────┐
│ │
YES NO
│ │
▼ ▼
PREVENT Exception
OPTIMIZE │
BULKIFY ▼
identify
cause
│
▼
can/should it
be handled?
│
┌────┴────┐
│ │
YES NO
│ │
▼ ▼
appropriate fix/
handling propagate
If You Want to Keep a Short Version in Your Head
Most Common Exceptions
| Problem | When it happens | Example | Does try/catch solve it? |
|---|---|---|---|
| Compile-time error | Before execution | Method does not exist | No |
NullPointerException |
Runtime | A null object is used |
Can be caught, but the cause should be investigated |
DmlException |
Runtime | insert or update failed |
Yes |
QueryException |
Runtime | Query result is incompatible with the operation | Yes |
ListException |
Runtime | Invalid index | Yes |
SObjectException |
Runtime | Invalid SObject usage | Yes |
TypeException |
Runtime | Incompatible type conversion | Yes |
StringException |
Runtime | Invalid String value for an operation | Yes |
MathException |
Runtime | Invalid mathematical operation | Yes |
JSONException |
Runtime | JSON processing problem | Yes |
CalloutException |
Runtime | Problem during a callout | Yes |
| Custom Exception | Runtime | Application-defined business rule | Yes |
System.LimitException |
Runtime | Governor Limit exceeded | Not with normal catch |
Less Frequent but Built-In Exceptions
| Exception | When it happens |
|---|---|
VisualforceException |
General problem in a Visualforce page |
NoAccessException |
Access denied to an sObject or field in a Visualforce context |
NoDataFoundException |
Access to a record that no longer exists in a Visualforce context |
SerializationException |
Data serialization failure, commonly in Visualforce contexts |
AsyncException |
Failure while queuing asynchronous work |
ExternalObjectException |
Problem accessing data through External Objects or Salesforce Connect |
FinalException |
Attempt to modify read-only data or a final
variable |
EmailException |
Email-related failure |
XmlException |
Error reading or writing XML through XmlStream |
SearchException |
SOSL-related problem through SOAP API search() |
SecurityException |
Problem in static methods of the Crypto class |
NoSuchElementException |
Access beyond the end of an Iterator |
InvalidParameterValueException |
Invalid parameter passed to a method |
RequiredFeatureMissing |
Required platform feature is not enabled in the org |
This list is not meant to represent every exception Salesforce may add in the future.
It gives you a practical mental base for the built-in exception types documented today.
In the End, Everything Comes Back to the Same Question
After going through NullPointerException,
DmlException, QueryException, CPU, heap, SOQL,
DML, and everything else, it may feel like we are talking about dozens
of unrelated problems.
But one question connects all of them:
What exactly happened to this transaction?
Did the code fail before it could start?
Then look at compilation, types, references, and structure.
Did it start and then find an unexpected state?
Then look at data, variables, flow, the exception message, and the stack trace.
Did it use too many resources?
Then do not look for a miracle inside catch.
Go back to architecture, volume, collections, queries, DML, and algorithm design.
That is what I would like you to take from this article.
Not a memorized list.
A way of thinking.
Because in practice, you do not need to recognize every message from memory to be a good Apex developer.
You need to know how to ask the next useful question.
The Next Time the Screen Turns Red
Before changing any line of code, try this sequence:
1. Did the code compile?
2. What is the exact exception type?
3. What is the complete message saying?
4. Where does the stack trace point?
5. Was the data on that line what I expected?
6. Is this a normal business situation or a bug?
7. Is a Governor Limit involved?
8. Does the fix solve the cause, or only hide the symptom?
If you do this, a message that used to look like "another Salesforce error" starts to become a diagnosis.
And that change is huge.
You stop fighting the red screen and start listening to what the platform is trying to tell you.
At first, it still takes time.
Later, you see:
Too many SOQL queries: 101
and you no longer think:
"Oh no, everything broke."
You think:
"Okay. Who is running too many queries in this transaction?"
And that is exactly the point where debugging starts to become less scary and much more interesting.
Official Salesforce Sources and Study Materials
Because the platform changes over time, especially limits and execution behavior, it is a good idea to keep official Salesforce documentation as the source of truth for specific numbers and rules.
Salesforce Developers: Handle Errors from Apex
https://developer.salesforce.com/docs/platform/lwc/guide/apex-error-handling.html
Official reference for built-in exceptions, custom exceptions,try/catch, safe navigation, andAuraHandledExceptionin Apex and LWC/Aura scenarios.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
Explains SOQL, DML, multitenancy, bulkification, rollback, and why Governor Limit exceptions are not solved withcatch.Salesforce Developers: Understanding the Bulk of the Salesforce Platform
https://developer.salesforce.com/blogs/engineering/2014/08/understanding-bulk-salesforce1-platform
Salesforce material about bulk processing, collections, and moving SOQL and DML out of loops.Salesforce Developers: Apex Code Best Practices
https://developer.salesforce.com/ja/wiki/apex_code_best_practices
Includes bulkification practices and examples of using theLimitsclass to monitor Governor Limit usage.Salesforce Developers: Handle Errors from Apex, LWC Developer Guide
Also check the linked error-handling and safe-navigation examples to understand how errors move between Apex and the frontend.
Publication note: before treating any Governor Limit number as an absolute truth, confirm the current value in Execution Governors and Limits in the Apex Developer Guide. Limits can depend on execution context and Salesforce can change them between releases.
