Exceptions & Error Handling

Custom exception class names must end in Exception and extend Exception.

10.1 try / catch / finally

try {
    Account a = [SELECT Id FROM Account WHERE Name = 'Does Not Exist'];   // QueryException: no rows
} catch (QueryException e) {
    System.debug(LoggingLevel.ERROR, 'Query failed: ' + e.getMessage());
} catch (Exception e) {
    System.debug('Something else: ' + e.getTypeName() + ' ' + e.getStackTraceString());
} finally {
    System.debug('Always runs');
}

10.2 Common built-in exceptions

Dereferencing a null variable Assigning a zero- or multi-row query to a single sObject

Validation rule failure, required field missing, duplicate value

Index out of bounds, duplicate IDs in a DML list Invalid cast or conversion (Integer.valueOf('abc')) Division by zero Invalid ID string, bad string operation

Governor limit exceeded — cannot be caught HTTP callout failure, callout after uncommitted DML Accessing a field that was not queried

User-mode or WITH SECURITY_ENFORCED access violation Thrown to send a friendly message to LWC/Aura clients

10.3 Custom exceptions

Custom exception class names must end in Exception and extend Exception. They automatically get four constructors: (), (String message), (Exception cause), (String message, Exception cause).

Custom exceptions can carry extra data:

public class InsufficientFundsException extends Exception {}

public class BankAccount {
    public Decimal balance { get; private set; }
    public BankAccount(Decimal opening) { balance = opening; }

    public void withdraw(Decimal amount) {
        if (amount > balance) {
            throw new InsufficientFundsException(
                'Cannot withdraw ' + amount + '; balance is ' + balance);
        }
        balance -= amount;
    }
}

// usage
try {
    new BankAccount(100).withdraw(500);
} catch (InsufficientFundsException e) {
    System.debug(e.getMessage());
}
public class IntegrationException extends Exception {
    public Integer statusCode;
    public IntegrationException(String msg, Integer code) {
        this(msg);              // call the built-in message constructor
        this.statusCode = code;
    }
}

10.4 Error-handling guidelines

Catch specific exceptions; avoid empty catch blocks that hide failures.

An unhandled exception rolls back the whole transaction (all DML) and shows an error to the user or sends an email to the developer (for async jobs).

In triggers, prefer record.addError('message') to block a single record with a friendly message rather than throwing.

LimitException can't be caught — prevent it by bulkifying and checking Limits (§21).

Log errors persistently (custom Log__c object, Platform Event-based logger, or an open-source framework such as Nebula Logger) rather than relying on debug logs.

🧪 Part I Exercises

Sign up for a Developer Edition org, install the Salesforce CLI and VS Code extensions, create a project, authorize the org, and deploy a HelloWorld class.

In Execute Anonymous, print the first 20 Fibonacci numbers using a Long and a while loop.

Write a static method isPalindrome(String s) that ignores case, spaces, and punctuation; test it with three inputs.

Given a List<Integer>, return a Map<String, List<Integer>> with keys 'even' and 'odd'.

Build a Set<String> of unique email domains from all Contacts in your org (substringAfter('@')), lowercased.

Write a method that returns the number of business days (Mon–Fri) between two Date values without using BusinessHours.

Create an enum Priority { LOW, MEDIUM, HIGH, CRITICAL } and a switch that maps each value to an SLA in hours.

Write Decimal applyDiscount(Decimal price, Decimal percent) that rounds to 2 places with HALF_UP and throws a custom InvalidDiscountException for percentages outside 0–100.

Demonstrate the difference between == and equals() for strings, and between ?. and normal dot access on a null sObject.

Write a test class with at least three test methods for exercises 3, 4, and 8 using the Assert class; run it from the CLI with code coverage.