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
Last updated: August 2026

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

ExceptionTypical cause
DmlExceptioninsert/update/delete/undelete failures (validation, required fields, duplicates)
QueryExceptionSOQL problems (no rows for assignment to single sObject, malformed dynamic SOQL issues)
NullPointerExceptionMethod/field access on null reference
ListExceptionBad list index
TypeExceptionInvalid conversion/cast
LimitExceptionGovernor limit exceeded
CalloutExceptionHTTP callout failures
JSONExceptionJSON 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

  1. Let the exception propagate so the transaction rolls back
  2. Catch, add a page/error message or log with correlation Id, then rethrow
  3. Use Database.update(records, false) partial-success APIs when business rules allow partial saves—and still process Database.SaveResult errors explicitly
  4. 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
Test Your Knowledge

What is the result of assigning a SOQL query that returns zero rows to a single Account variable?

A
B
C
D
Test Your Knowledge

Which statement best describes a SOQL-for loop compared with assigning the full query to a List?

A
B
C
D
Test Your Knowledge

Why is an empty catch (Exception e) { } block considered a poor practice in Apex?

A
B
C
D