Apex does not come with a pause button. There is no traditional breakpoint that stops execution and lets you poke around. What Salesforce gives you instead is a set of logging, inspection, and replay tools that, used well, make finding the problem faster than most developers expect.
This guide covers the practical workflow from first error to confirmed fix, with examples at each step.
Step 1: read the error before you do anything else
Most debugging sessions start with someone jumping straight into the code. The error message is still the fastest path to the problem, and it is worth reading carefully before opening any tool.
Salesforce Apex errors typically tell you the class name, the line number, and the exception type. A System.NullPointerException at line 42 in AccountTriggerHandler is telling you exactly where to look. A System.LimitException: Too many SOQL queries: 101 is telling you the problem is structural, not a typo.
The three most common Apex exceptions and what they signal:
- NullPointerException A variable you expected to have a value is null. Usually means a query returned no records, or an object field was never set.
- LimitException A governor limit was hit. SOQL inside a loop is the classic cause of the 101 SOQL queries error.
- DmlException A database operation failed. Required field missing, validation rule violation, or duplicate record are the usual culprits.
Step 2: add System.debug() to trace what is happening
System.debug() is the starting point for most Apex debugging. It writes output to the debug log, which you can then read to understand what your code was actually doing at each point.
Place debug statements before and after the line you suspect is failing, and log the values of the variables involved.
Basic System.debug() usage
Account acc = [SELECT Id, Name, OwnerId FROM Account WHERE Id = :accountId LIMIT 1];
System.debug('Account retrieved: ' + acc);
System.debug('Owner ID: ' + acc.OwnerId);
The LoggingLevel parameter controls how much noise you produce in the log. Use LoggingLevel.DEBUG for general tracing and LoggingLevel.ERROR for conditions that should never occur.
Using log levels to filter output
System.debug(LoggingLevel.DEBUG, 'Entering processAccount method');
System.debug(LoggingLevel.ERROR, 'Account is null - this should not happen');
System.debug(LoggingLevel.FINEST, 'Loop iteration value: ' + i);
Setting the log level to FINEST captures everything. That is useful when you have no idea where the problem is. Once you narrow the location down, reduce it to DEBUG to keep the log readable.
Step 3: enable debug logs and read them in Developer Console
Debug statements only appear if logging is active for the user running the code. Setting up a trace flag takes two minutes and is required before any logging will appear.
- Go to Setup search for Debug Logs in the Quick Find box.
- Click New select the user you want to trace, set an expiry time, and choose a debug level. Start with SFDC_DevConsole if you are not sure which level to use.
- Reproduce the issue trigger the code by running the action that causes the error.
- Open Developer Console go to the Logs tab and double-click the most recent log entry.
- Use the filter type the class or method name you are debugging to isolate relevant lines from the noise.
The log output includes a timestamp, event type, and the message from your System.debug() call. What to look for:
- USER_DEBUG lines contain your System.debug() output
- SOQL_EXECUTE lines show every query that ran and how many rows it returned
- DML_BEGIN and DML_END wrap every database operation
- CUMULATIVE_LIMIT_USAGE near the end of the log shows governor limit consumption
Step 4: use Execute Anonymous to isolate and test
When you want to test a specific piece of logic without triggering the full context of a trigger or flow, Execute Anonymous is the fastest tool available. It runs Apex directly in the org against real data, and the debug log appears immediately.
Open it from Developer Console under Debug, then Open Execute Anonymous Window.
Testing a query and inspecting results in isolation
Id testAccountId = '0015g00000XXXXXX';
List<Account> accts = [
SELECT Id, Name, AnnualRevenue, OwnerId
FROM Account
WHERE Id = :testAccountId
];
System.debug('Records found: ' + accts.size());
if (!accts.isEmpty()) {
System.debug('Account name: ' + accts[0].Name);
System.debug('Annual revenue: ' + accts[0].AnnualRevenue);
}
This approach lets you confirm whether the data you expect to be there actually is, before assuming the logic is wrong. Many debugging sessions end here because the data was the problem, not the code.
Step 5: use Apex Replay Debugger for complex flows
For situations where System.debug() alone is not enough, VS Code’s Apex Replay Debugger lets you step through a captured debug log line by line, inspect variable values at each point, and set breakpoints on specific lines without stopping real execution.
This is the closest Apex gets to a traditional debugger, and it is free with the Salesforce Extension Pack for VS Code.
- Open the Apex class or trigger in VS Code click in the gutter to the left of the line numbers to set a breakpoint.
- Enable the replay debugger open the Command Palette with Ctrl+Shift+P and run SFDX: Turn On Apex Debug Log for Replay Debugger.
- Reproduce the issue in the org trigger the code that is failing.
- Fetch the log Command Palette, then SFDX: Get Apex Debug Logs, and select the relevant log.
- Launch the replay Command Palette, then SFDX: Launch Apex Replay Debugger with Current File. Execution will pause at your breakpoints and you can inspect variable values in the VS Code sidebar.
Replay Debugger does not run code again. It replays a captured log, so the variable values you see reflect what actually happened during the original execution. This is exactly what makes it useful for reproducing intermittent bugs.
Step 6: identify governor limit issues before they become production problems
Governor limits are the category of Apex errors that can reach production undetected in low-volume testing and then fail at scale. The most common is the 101 SOQL queries limit caused by queries inside loops.
The pattern that causes 101 SOQL queries
// This will hit the limit with more than 100 opportunities
for (Opportunity opp : oppList) {
Account acc = [SELECT Id, Name FROM Account WHERE Id = :opp.AccountId];
// one query per iteration
}
The correct pattern: query outside the loop
// Collect all AccountIds first
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : oppList) {
accountIds.add(opp.AccountId);
}
// Single query for all records
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
// Now iterate using the map
for (Opportunity opp : oppList) {
Account acc = accountMap.get(opp.AccountId);
}
The CUMULATIVE_LIMIT_USAGE section at the end of every debug log shows exactly how many queries, DML statements, and CPU milliseconds your transaction consumed. Check it any time you are unsure whether your code is approaching a limit.
Step 7: write a unit test that reproduces the bug
Once you have identified the cause, write a unit test that fails with the bug present and passes after the fix. This has two benefits: it confirms the fix is correct, and it prevents the same bug from returning in a future deployment.
A test that reproduces a specific scenario
@isTest
private class AccountTriggerHandlerTest {
@isTest
static void testProcessAccountWithNoRevenue() {
Account acc = new Account(Name = 'Test Corp');
// AnnualRevenue deliberately left null to reproduce the bug
Test.startTest();
insert acc;
Test.stopTest();
Account result = [SELECT Id, Rating FROM Account WHERE Id = :acc.Id];
System.assertEquals('Cold', result.Rating,
'Expected Rating to default to Cold when revenue is null');
}
}
Running tests in VS Code with the Salesforce CLI produces both pass/fail results and code coverage data. A test that specifically targets the bug scenario is considerably more useful than a test that covers the happy path and happens to pass the coverage threshold.
Quick reference: which tool for which situation
- Bug reported in production, no reproduction steps Enable a trace flag for the affected user, ask them to reproduce the action, then read the log.
- Need to test a query or small code change quickly Use Execute Anonymous in Developer Console.
- Complex logic with multiple method calls and unclear failure point Use Apex Replay Debugger in VS Code with breakpoints on the suspect methods.
- Code works in sandbox but fails at scale Check CUMULATIVE_LIMIT_USAGE in the log for governor limit consumption. The pattern is almost always SOQL or DML inside a loop.
- Intermittent error that is hard to reproduce Add detailed System.debug() statements at every branch point, enable logging for the affected user permanently for a period, and review logs after the next occurrence.
Debugging Apex gets faster with repetition. The same patterns show up repeatedly: null records, SOQL in loops, missing field permissions, data that looked correct in testing and was not in production. Learning to read a debug log efficiently is the single skill that shortens most of those investigations.
TrueSolv works with development teams to build cleaner Apex, better test coverage, and more maintainable orgs. Follow TrueSolv on LinkedIn for practical Salesforce development guidance.





