Skip to main content

Salesforce Exception Handling & Logging: Best Practices & Patterns

๐Ÿ’ฌ In plain words: Let exceptions bubble up to one central place. Catch them there, and permanently write the error to a custom Log__c record via a Platform Event so the log survives a database rollback. An empty catch block is how errors vanish without a trace—and haunt you in production.
๐Ÿ“Œ Real-Life Example: The Silent Integration Failure
Imagine an integration that fails silently for a week because someone wrote catch(Exception e){}. To fix this, catch the error once at the top level and publish an Error_Event__e carrying the message and record ID. A subscriber then inserts an Integration_Log__c. Now, even if the main transaction rolls back, the rollback can't erase the evidence.

Key Points at a Glance

  • Never Empty-Catch: Catch an exception to act on it (log it, recover, or notify), never to silence it.
  • Survive Rollbacks: Standard DML logs get erased during a transaction rollback. Use Platform Events to decouple the log writing process.
  • User-Friendly Errors: Always throw an AuraHandledException to present clean, readable error messages to LWC and Aura components.
  • Finalizers for Uncatchables: Use Transaction Finalizers to catch and log LimitExceptions that bypass traditional try-catch blocks.

Core Concepts of Exception Handling

Robust Apex separates recoverable errors from unrecoverable ones. It NEVER swallows an exception silently.

  • Custom Exceptions: By extending the standard Exception class, you can create typed, catchable domain errors specific to your business logic.
  • UI Error Handling: A raw exception leaks server-side internals. AuraHandledException ensures a clean message surfaces to the UI.
  • Limit Exceptions: Uncatchable limits (like hitting the SOQL 100 limit) bypass catch blocks entirely. Only careful pre-checks or modern Transaction Finalizers can handle those gracefully.
  • Production Logging: Production orgs require a persistent logging framework. Use a custom object, fed by Platform Events, to capture stack traces, context, and severity. Surface these logs on an admin dashboard.
Exception Handling & Custom Logging
๐Ÿง  Core Takeaway: Never empty-catch. Use AuraHandledException for a clean UI message. LimitException bypasses catch, so use Finalizers. Log via Platform Events so logs survive rollbacks.
๐Ÿงญ 360 Card — Exception Handling & Logging
  • Rule: Never swallow an exception. Catch once, at the top level, and write it somewhere permanent.
  • Gain: You establish one central place that turns any failure into a logged record and a friendly UI message.
  • Price: A normal DML log dies with the rollback that killed the transaction. You need a separate channel that survives it.
  • Limits: Publish a Platform Event to log immediately. Uncatchable limits require a Finalizer.
  • Mirror Trap: Wrapping try-catch blocks everywhere feels careful, but a catch that swallows the error and carries on is far worse than no catch at all.
  • At Volume: Tier your log severity, otherwise you will get alert fatigue. ERROR means something broke; WARN means performance degraded but was handled.
⚠ INTERVIEW TRAP: catch(Exception e){} is worse than no catch at all. It makes bugs invisible. An integration might fail silently for a month before a customer complains. Catch once, at the top, and log it.

Building a Rollback-Proof Logger

Here is how you decouple logging using Platform Events so the log survives even if the main transaction is rolled back:

// 1. One place that records every error, so no failure is silent.
public class LogEvent {
    public static void error(String ctx, Exception e) {
        
        // 2. Publish a platform event. It survives the rollback.
        EventBus.publish(new Log__e(
            Context__c = ctx,
            Message__c = e.getMessage(),
            // 3. Keep the stack trace. Without it you know something broke but not where.
            Stack__c   = e.getStackTraceString(),
            Severity__c = 'ERROR'
        ));
    }
}

Core Q&A and Common Scenarios

Q: How do you design error logging that survives a transaction rollback?
๐ŸŽฏ Say this first: "Publish an immediate platform event carrying the error. Let a subscriber insert the Log__c record. Events survive the rollback that kills a direct DML insert."

A: The common trap is writing a Log__c record using normal DML inside the failing transaction. The database rollback erases your log along with everything else.

The fix is to publish a Platform Event. Use EventBus.publish (configured for "Publish Immediately"). A trigger on that event inserts the log in a separate transaction. This is the gold standard, utilized by frameworks like Nebula Logger. For async work, modern Transaction Finalizers guarantee a log write even when uncatchable limit exceptions occur.

Q: A user sees 'System.NullPointerException' in an LWC toast. What went wrong and how do you fix the UX?

A: Raw Apex exceptions were thrown straight to the client. The backend method should catch the raw exception and rethrow it as an AuraHandledException with a user-friendly message, while logging the real exception server-side for the engineers.

The user should see something like, "We couldn't load your cases — please retry or contact support." Never let internal exception text reach an end-user. It is bad UX and a potential security vulnerability (information disclosure).

Q: How do you decide what severity a caught error is, and avoid alert fatigue?

A: Tier your logs by how actionable they are:

  • ERROR: Something broke that shouldn't have. It needs immediate attention and should route to real-time alerts.
  • WARN: Degraded but handled (e.g., a retry succeeded, or a fallback was used). Aggregate these into a daily digest.
  • INFO: Notable but normal. Keep it queryable but silent.

Alert fatigue kills logging frameworks. Add rate-limiting and deduplication. If 100 identical NPEs fire from a single bad batch, they should produce one alert, not 100.

Q: How do you roll back part of a transaction, and what are the Savepoint traps?

A: Database.setSavepoint() marks a point in time, and Database.rollback(sp) undoes the DML done since that point while letting the transaction carry on. However, watch out for these traps:

  • Limits are not refunded: A rollback does NOT give you back your consumed SOQL rows or statements. Furthermore, setting a savepoint and rolling back each cost a DML statement.
  • Stale IDs: Records inserted before the rollback keep their IDs in your variables, even though the database rows no longer exist. You must clear those IDs before attempting to re-insert them.
  • Savepoint Invalidation: Rolling back to an earlier savepoint invalidates any savepoint set after it.
  • Callout Exceptions: Making an HTTP callout after setting a savepoint throws a CalloutException. Always structure your code to call out before the savepoint or after the work is fully committed.
Q: Try-catch everywhere vs. letting it fail — which is right?

A: Catch only where you can DO something: add context, log it, turn it into a friendly message, or clean up state. Rule: Catch to act, never to silence. Hiding bugs is much worse than letting a process crash cleanly.

Q: How do you capture the ID of the failing record in your error log?

A: There are four main patterns depending on the context:

  • Standard Catch: You already hold the record in memory. Copy record.Id into the RecordId__c field of your log event.
  • Partial Success (Database.update): When using Database.update(records, false), the SaveResult list matches the input list order. Loop through with an index. When result.isSuccess() is false, grab the ID using records[i].Id.
  • Failed Inserts: A failed insert has no ID because Salesforce never assigned one. Log the list index alongside a unique business key (like an External ID or email address).
  • Salesforce Flow: Pass $Record.Id from the Fault Path directly into your logging subflow.

Pro Tip: Always store Request.getCurrent().getQuiddity() in your logs. It tells you exactly where the code ran (trigger, batch, REST API, or anonymous apex), turning "something failed" into "this specific record failed inside a batch job."