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/catch will 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:

  1. Which reference is null?
  2. Could it be null here, 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 that insert failed;
  • 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;
  • null values;
  • 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.

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.

  1. You tried to modify a record that is read-only. A classic example is changing Trigger.new inside an after insert or after update trigger.
trigger AccountTrigger on Account (after update) {
    for (Account acc : Trigger.new) {
        acc.Name = 'New Name'; // FinalException:
                               // Record is read-only
    }
}
  1. 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:

  1. Is the expected behavior correct?
  2. Is the test data correct?
  3. Is the production code returning the wrong result?
  4. 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:

  1. How many queries did I execute?
  2. 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 @future method 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.

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.