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.
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
AuraHandledExceptionto 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
Exceptionclass, you can create typed, catchable domain errors specific to your business logic. - UI Error Handling: A raw exception leaks server-side internals.
AuraHandledExceptionensures 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.
- 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.
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
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.
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).
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.
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.
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.
A: There are four main patterns depending on the context:
- Standard Catch: You already hold the record in memory. Copy
record.Idinto theRecordId__cfield 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. Whenresult.isSuccess()is false, grab the ID usingrecords[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.Idfrom 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."