6.3 Control Flow & Exception Handling
Key Takeaways
- Apex supports if/else, switch on, traditional for, list for-each, SOQL-for loops, and while
- switch on works with enums, Integers, Longs, Strings, Ids, and sObject types—with when else for default
- try/catch/finally handles recoverable failures; finally runs whether or not an exception occurred
- Know DmlException, QueryException, NullPointerException, and LimitException (and that LimitException cannot be caught in the usual way for governor limits in many contexts)
- Do not swallow exceptions with empty catch blocks—log, rethrow as meaningful custom exceptions, or fail the transaction intentionally
6.3 Control Flow & Exception Handling
Quick Answer: Apex control flow uses if/else, switch on, for (index, list, SOQL-for), and while. Handle failures with try/catch/finally, catch specific built-ins such as DmlException and QueryException, and avoid empty catch blocks that hide production bugs. Custom exceptions communicate domain errors; governor LimitException behavior is a special exam-aware topic.
Reading and predicting snippet outcomes is a large part of Platform Developer I. This section trains the constructs those snippets use.
if / else
if (opp.Amount != null && opp.Amount > 100000) {
opp.Priority__c = 'High';
} else if (opp.Amount != null && opp.Amount > 10000) {
opp.Priority__c = 'Medium';
} else {
opp.Priority__c = 'Standard';
}
Conditions must be Boolean. Null-safe comparisons matter: comparing null Amount without a null check can yield unexpected branches. Prefer explicit null tests for optional fields.
switch on
switch on is cleaner than long if/else chains for discrete values:
switch on account.Type {
when 'Customer - Direct', 'Customer - Channel' {
// multiple when values share a body
}
when 'Partner' {
// partner logic
}
when null {
// Type not set
}
when else {
// default
}
}
You can switch on enums, Integer, Long, String, Id, and certain type expressions. Each when body does not fall through to the next (no C-style fall-through). Use when else as the default branch.
Exam tip: when null is valid and useful for optional fields—do not assume null is handled by when else only; null can be matched explicitly.
for Loops: Three Exam Shapes
1. Traditional (index) for
for (Integer i = 0; i < items.size(); i++) {
Item__c row = items[i];
row.Sequence__c = i + 1;
}
Use when you need the index or must step by non-one increments.
2. List / set iteration for
for (Contact c : Trigger.new) {
c.Description = 'Processed';
}
Most readable for bulk trigger logic. Works with Lists and Sets.
3. SOQL-for loop
for (Account a : [SELECT Id, Name FROM Account WHERE Industry = 'Energy']) {
// platform retrieves records in chunks
}
SOQL-for loops are preferred for large result sets because they process records in batches of up to 200 and reduce heap pressure compared with assigning the entire result to a List at once. Still respect governor limits—do not put DML or SOQL inside the loop without bulk patterns.
while
Integer i = 0;
while (i < 5) {
i++;
}
Use for polling-style logic or when the stop condition is not a simple collection size. Infinite loops burn CPU and hit governor limits—always ensure progress toward exit.
Exception Handling: try / catch / finally
try {
insert accounts;
} catch (DmlException e) {
System.debug('DML failed: ' + e.getMessage());
// optionally inspect e.getNumDml(), e.getDmlMessage(i), e.getDmlFieldNames(i)
} catch (Exception e) {
// broader fallback—order specific catches before generic Exception
throw e; // rethrow if you cannot handle
} finally {
// always runs: cleanup, logging markers, reset static flags carefully
}
Order matters: place more specific catch types before Exception. Once a catch matches, later catches for the same try do not run.
finally executes after try/catch whether the try succeeded, caught, or rethrew (unless the process is terminated in extreme ways). Use it for guaranteed cleanup, not for business success logic that should only run on happy path.
Built-in Exceptions Worth Memorizing
| Exception | Typical cause |
|---|---|
| DmlException | insert/update/delete/undelete failures (validation, required fields, duplicates) |
| QueryException | SOQL problems (no rows for assignment to single sObject, malformed dynamic SOQL issues) |
| NullPointerException | Method/field access on null reference |
| ListException | Bad list index |
| TypeException | Invalid conversion/cast |
| LimitException | Governor limit exceeded |
| CalloutException | HTTP callout failures |
| JSONException | JSON serialize/deserialize errors |
Single-sObject query trap:
Account a = [SELECT Id FROM Account WHERE Name = 'Missing']; // 0 rows → QueryException
List<Account> listForm = [SELECT Id FROM Account WHERE Name = 'Missing']; // OK, empty list
Assigning a query to a single sObject requires exactly one row. Zero rows or more than one row throws QueryException. Prefer List assignment when cardinality is uncertain.
LimitException: Governor limit exceptions indicate the transaction has violated platform caps (SOQL count, CPU, heap, DML rows, etc.). Design bulk-safe code so you do not rely on catching limits as control flow. Exam questions favor prevention (bulkification) over “catch LimitException and continue.”
Custom Exceptions
Define domain-specific errors by extending Exception:
public class InvoiceValidationException extends Exception {}
public static void validate(Invoice__c inv) {
if (inv.Amount__c == null || inv.Amount__c <= 0) {
throw new InvoiceValidationException('Amount must be positive');
}
}
Custom exceptions document intent, allow selective catch blocks, and keep service layers from overusing generic Exception for every business rule failure.
When Not to Swallow Exceptions
Empty or silent catch blocks are an anti-pattern:
try {
update records;
} catch (Exception e) {
// BAD: swallows failure; caller thinks success
}
Problems:
- Users see success while data did not save
- Tests pass for the wrong reason
- Debugging production issues becomes impossible
- Partial automation chains continue with corrupt state
Better options
- Let the exception propagate so the transaction rolls back
- Catch, add a page/error message or log with correlation Id, then rethrow
- Use
Database.update(records, false)partial-success APIs when business rules allow partial saves—and still processDatabase.SaveResulterrors explicitly - Convert to a custom exception with a clear message for upper layers
Catching is appropriate when you can recover (retry with backoff in async contexts, fallback value, skip an optional enrichment) or when you must translate the error for the UI. Catching only to hide failures is never correct.
Combining Control Flow with Bulk Patterns
Pseudo-structure for trigger handlers:
try {
Set<Id> ids = new Set<Id>();
for (SObject row : newRecords) { /* collect */ }
Map<Id, Account> related = new Map<Id, Account>([SELECT ... WHERE Id IN :ids]);
List<Account> toUpdate = new List<Account>();
for (SObject row : newRecords) {
// if/switch business rules using map lookups
}
if (!toUpdate.isEmpty()) update toUpdate;
} catch (DmlException e) {
// surface or rethrow—do not empty-catch
throw e;
}
Control flow chooses branches; collections keep those branches bulk-safe. Exception handling decides failure visibility—not whether bulkification is required.
Exam Checklist
- Predict outcomes of if/switch with null values
- Know single-sObject SOQL vs List SOQL exception behavior
- Prefer specific catch types and meaningful handling
- Never choose “empty catch” as a best practice
- SOQL-for for large queries; list for for Trigger.new; traditional for when index matters
What is the result of assigning a SOQL query that returns zero rows to a single Account variable?
Which statement best describes a SOQL-for loop compared with assigning the full query to a List?
Why is an empty catch (Exception e) { } block considered a poor practice in Apex?