Study guide
6.3 Control Flow & Exception Handling
Salesforce Certified Platform Developer I Exam
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Blueprint Weights & Study Strategy
2 Platform Fundamentals
2.1 Multi-Tenant Architecture & Shared Resources 2.2 MVC Mapping on the Lightning Platform 2.3 Declarative vs Programmatic Decisions 2.4 Agentforce for Developers: Use Cases & Limitations
3 Data Modeling & Schema
3.1 Objects, Fields & Schema Design 3.2 Relationship Types & Implications 3.3 Schema Design Impact on Apex 3.4 Data Import & Export for Development
4 Platform Data Features
4.1 Formula Fields & Roll-Up Summaries 4.2 Core CRM Objects & Standard Schema 4.3 AppExchange Extensions & Platform Boundaries
5 Declarative Process Automation
5.1 Record-Triggered Flows & Declarative Automation 5.2 Approval Processes & Business Rules 5.3 When to Use Apex vs Declarative Tools
6 Apex Language Basics
6.1 Variables, Constants & Apex Data Types 6.2 Lists, Sets & Maps 6.3 Control Flow & Exception Handling 6.4 Classes, Interfaces & OOP Patterns
7 SOQL, SOSL & DML
7.1 SOQL Query Fundamentals 7.2 SOSL Search Fundamentals 7.3 DML Operations & Bulkification 7.4 Governor Limits & Transaction Boundaries
8 Apex Triggers & Order of Execution
8.1 Trigger Structure & Context Variables 8.2 Bulk Trigger Patterns & Best Practices 8.3 Save Order of Execution 8.4 Recursion, Cascading & Transaction Control
9 Asynchronous Apex
9.1 @future Methods 9.2 Queueable Apex 9.3 Batch Apex 9.4 Schedulable Apex & Job Orchestration
10 Security, Events & Schema Access
10.1 Sharing, FLS, CRUD & Injection Prevention 10.2 Platform Events Publish & Subscribe 10.3 Programmatic Schema Access (Describe)
11 Visualforce
11.1 Visualforce Pages & Markup 11.2 Standard, Custom Controllers & Extensions 11.3 Visualforce in Lightning Experience
12 Lightning Components
12.1 Lightning Component Framework Benefits 12.2 Aura Component Structure & Resources 12.3 Lightning Web Components Fundamentals 12.4 LWC Data Access: Wire, Imperative & Events
13 UI Composition & Apex Integration Patterns
13.1 Composing UI in Lightning Apps 13.2 UI Security, Performance & Accessibility Patterns 13.3 Apex Actions for Flow, LWC, and Agentforce
14 Apex Testing
14.1 Testing Framework & Coverage Requirements 14.2 Writing Unit Tests for Triggers & Classes 14.3 Test Data Strategies & Isolation 14.4 Executing Tests & Execute Anonymous Differences
15 Debugging & Developer Tools
15.1 Debug Logs & Monitoring 15.2 Developer Console, CLI, VS Code & Workbench 15.3 Sandboxes, Scratch Orgs & Environment Strategy
16 Deployment & Release
16.1 Change Sets & Metadata Deployment 16.2 Unlocked Packages, Metadata API & Salesforce DX 16.3 Release Readiness & Credential Maintenance
6.3 Control Flow & Exception Handling
Key Takeaways Explain This Topic AI
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
- 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 process Database.SaveResult errors 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