Pages

Saturday, August 1, 2026

9 Apex Errors Every Salesforce Developer Will Hit (And How to Actually Fix Them)

I've been building on Salesforce long enough to say this without exaggerating: I've seen every error on this list crash a production org at least once. Not in a sandbox, not in a code review — in front of a client, during a demo, or worse, during month-end close when Finance is watching the screen. That's usually when you actually learn what an error message means, because "I'll Google it later" isn't an option anymore.

So this isn't a copy-paste of the Apex documentation. It's the list I wish someone had handed me early in my career — the errors that show up again and again, why they actually happen (not just what the stack trace says), and how to fix them without introducing a new bug while you're at it. If you're an admin who just started writing Apex, or a dev who's been doing this for years and still winces at a certain red error banner — this one's for you.

The Cheat Sheet (Bookmark This)

Error MessageWhat's Really Going OnThe Fix
Attempt to de-reference a null objectYou assumed something existed. It didn't.Null-check before you touch it
Too many SOQL queries: 101A query snuck inside a loopPull it out, query once, use a map
Too many DML statements: 151Same crime, different weapon — DML in a loopCollect the records, DML once
FIELD_CUSTOM_VALIDATION_EXCEPTIONYour own validation rule is doing exactly its jobFix the data, or build a bypass for automation
List has no rows for assignment to SObjectYou assumed a record existed. It didn't.Query into a list, check if it's empty
List index out of bounds: 0Same story, one more time — assumed data that isn't therelist.isEmpty() before you index it
INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITYNot a code bug — it's sharing/FLS doing its jobCheck sharing rules and field access for that profile
REQUIRED_FIELD_MISSINGYou built the object but forgot a field it can't live withoutPopulate it, especially on related/child records
Apex CPU time limit exceededSomething's doing way more work than it shouldDebug log, find the loop, bulkify or go async
MIXED_DML_OPERATIONYou mixed setup objects (User, Group) with regular ones in one transactionSystem.runAs() or push it to a @future
UNABLE_TO_LOCK_ROWTwo processes fighting over the same record at the same timeShorten transactions, add retry logic
Maximum trigger depth exceededYour trigger is chasing its own tailA static recursion guard, every time

1. "Attempt to de-reference a null object"

This is the error every Salesforce developer meets on day one and never fully escapes. It just means you tried to use something that doesn't exist yet. Simple as that. The tricky part isn't understanding it — it's remembering to guard against it every single time, especially with relationship fields.

Account acc = [SELECT Id, ParentId FROM Account WHERE Id = :someId];
System.debug(acc.Parent.Name); // 💥 no Parent? this blows up

My honest advice: don't just fix the one line that broke — go find every other place in the class doing the same risky reach, because if it happened once it's probably lurking somewhere else too.

System.debug(acc.Parent?.Name); // returns null, doesn't throw

2. "Too Many SOQL Queries: 101"

I still remember the first time I caused this in a real org — a "small" trigger update that queried inside a for loop, worked perfectly in my testing with three records, and then someone did a bulk data load of 500. That's the thing about Apex: it will forgive you at small scale and expose you at scale. Always code like 200 records are coming, because eventually they will.

// This works fine... until it doesn't
for (Contact c : contactList) {
    Account a = [SELECT Id FROM Account WHERE Id = :c.AccountId];
}

// This works at any volume
Set<Id> accountIds = new Set<Id>();
for (Contact c : contactList) {
    accountIds.add(c.AccountId);
}
Map<Id, Account> accountMap = new Map<Id, Account>(
    [SELECT Id FROM Account WHERE Id IN :accountIds]
);

3. "Too Many DML Statements: 151"

Same disease, different symptom. If you find one of these in a codebase, check the file for the other one too — they travel together more often than not.

// Don't do this
for (Opportunity o : oppList) {
    o.StageName = 'Closed Won';
    update o;
}

// Do this instead
for (Opportunity o : oppList) {
    o.StageName = 'Closed Won';
}
update oppList;

4. FIELD_CUSTOM_VALIDATION_EXCEPTION

Here's the thing I tell every junior dev who panics when they see this one: this isn't your bug. Salesforce is telling you a validation rule fired and stopped the save — on purpose. Read the message carefully, because it usually spells out exactly which rule and why.

The real question is whether the save should have been blocked. If a customer is legitimately entering bad data, fix the data. If your automation is trying to do something the business rules weren't designed to allow, you need a deliberate bypass — a custom permission or a flag field the rule explicitly excludes — not a workaround that quietly disables validation for everyone.

5. "List Has No Rows for Assignment to SObject"

This one bites people who assume a lookup query will always return something. It won't. Ever. Especially not three months after you wrote the code, when someone deletes the "test" record your query was quietly depending on.

// Assumes success. Don't.
Contact c = [SELECT Id FROM Contact WHERE Email = :emailAddress];

// Assumes nothing. This is the way.
List<Contact> contacts = [SELECT Id FROM Contact WHERE Email = :emailAddress LIMIT 1];
Contact c = contacts.isEmpty() ? null : contacts[0];

6. INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY

This is the error that makes people question their own code when the code was never the problem. It's almost always sharing or field-level security — the running user genuinely can't see a related record, and Salesforce is enforcing that correctly.

Before you touch a single line of Apex, go check the profile's field-level security and the sharing rules on the parent object. I've watched developers burn an entire afternoon "fixing" logic that was working exactly as written, because the real issue was a permission set that never got assigned.

7. MIXED_DML_OPERATION

Salesforce won't let you touch a "setup" object (User, Group, GroupMember, and friends) and a regular object in the same transaction. It's a platform guardrail, not a bug — and it catches even experienced developers off guard.

// Fails — mixing a setup object with a non-setup object
update someAccount;
insert new GroupMember(GroupId = g.Id, UserOrGroupId = u.Id);

// Split the setup-object work into its own context
System.runAs(new User(Id = UserInfo.getUserId())) {
    insert new GroupMember(GroupId = g.Id, UserOrGroupId = u.Id);
}
// or hand it off to a @future method

8. Apex CPU Time Limit Exceeded

This one rarely has a single obvious cause — it's usually death by a thousand cuts. A nested loop here, an unbulkified query there, a formula recalculating something it didn't need to. My approach every time: turn on debug logs at FINEST for Apex, and actually read the limit usage lines instead of guessing. The log will point at the expensive part far faster than staring at the code will.

9. Maximum Trigger Depth Exceeded (Trigger Recursion)

An update fires a trigger, which updates the record, which fires the trigger again. It's a loop that only stops because Salesforce eventually forces it to. I put a recursion guard in almost every trigger handler I write now, out of habit more than necessity — it's cheap insurance against a problem that's genuinely painful to debug once it's live.

public class TriggerRecursionGuard {
    private static Boolean hasRun = false;

    public static Boolean shouldRun() {
        if (hasRun) {
            return false;
        }
        hasRun = true;
        return true;
    }
}

// Inside your trigger handler
if (TriggerRecursionGuard.shouldRun()) {
    // do the update that would otherwise re-fire the trigger
}

How I Actually Troubleshoot These

Every one of these errors funnels into roughly the same decision tree in my head. I turned it into a diagram because it's easier to follow at 6pm on a Friday than a wall of text is.

Apex Error ThrownRead the full error text(line number + exact message — don't skim it)Is it aGovernor Limit error?YesThere's a loop somewhere.Find it. Bulkify it.NoIs it a DmlException(validation / access / lock)?It's probably not "your"bug — check validation rules,FLS, and sharing firstNo (Null/Query/Index)You assumed data thatwasn't there. Null-check /LIMIT 1 / isEmpty() it.Still stuck? Pull a Debug Log(log level FINEST for Apex, not a guess)Write a test that fails first,then fix it — so it can't come back quietly

Habits That Keep Me Out of This List

None of this is groundbreaking. It's mostly discipline, repeated on every single class, even the ones that feel too small to matter.

  • Bulkify like it's non-negotiable. Because it is. Assume 200 records every time, not because the platform demands it in every case, but because "just this once" is exactly how these bugs get into production.
  • Default to with sharing. Only drop it when you have a real, documented reason — not because it made an error go away faster.
  • Null-check like you don't trust the data. Because you shouldn't. Use ?. and ?? liberally — they're cheap and they save you from 2am pages.
  • Give the front end a real error message. Wrap DML in try/catch, re-throw as AuraHandledException with something a human can actually act on — not "an error occurred."
  • Put a recursion guard in every trigger handler. It costs you three lines of code and saves you a very bad afternoon later.
  • Write the failing test before the fix. It's the difference between "I fixed it" and "I fixed it and I can prove it, and it can't sneak back in on the next deploy."
  • Read the whole stack trace. Not the headline, the whole thing. The class name and line number are handed to you — use them before you start guessing.
  • Turn on the debug log before you start troubleshooting, not after twenty minutes of frustration. It tells you exactly where the limits are being burned instead of making you guess.

Bottom Line

Most of what looks like "Apex being difficult" is really the platform enforcing the same handful of rules over and over: don't assume data exists, don't loop your queries, respect the security model, and don't fight the platform's guardrails around setup objects. Once those become muscle memory, this whole list mostly stops happening to you — and when it does, you'll recognize it in about four seconds instead of forty-five minutes.

No comments: