The short answer is technically yes, practically no. System.LimitException extends Exception, so a try/catch block can intercept it — but the transaction is already poisoned. Any DML, callouts, or further SOQL in the catch block will either fail or be rolled back when the transaction ends.
What actually happens
When a governor limit is breached, Salesforce throws System.LimitException. The runtime then marks the transaction for forced rollback — every uncommitted DML reverts, and certain operations are blocked for the rest of the transaction.
try {
for (Integer i = 0; i < 200; i++) {
Account a = [SELECT Id FROM Account LIMIT 1]; // crashes at 101
}
} catch (System.LimitException e) {
System.debug('Caught: ' + e.getMessage());
// We're caught, but the transaction is already doomed.
insert new Log__c(Message__c = e.getMessage()); // will NOT commit
}
The insert looks like it runs, but when the transaction unwinds, it’s rolled back along with everything else.
What you can still do in the catch block
A short list:
- Log to
System.debug— debug output isn’t transactional; it appears in the log. - Read fields off the exception —
getMessage,getStackTraceString,getTypeName,getLineNumber. - Set in-memory state — instance fields, static variables. These die at transaction end but can be inspected during testing.
What you cannot reliably do:
- DML (insert/update/delete) — won’t survive the rollback.
- Callouts — almost always blocked.
- Enqueue another async job — depends on the limit but usually blocked.
EventBus.publishof a standard platform event — also rolled back. Only publish-immediately events (those markedPublish Immediately) survive a rollback, which is exactly why they exist.
The right pattern: prevent, don’t recover
Because you can’t truly recover from a LimitException, the production approach is defensive limit-checking before the risky operation:
public static void enqueueIfRoom(Queueable job) {
if (Limits.getQueueableJobs() < Limits.getLimitQueueableJobs()) {
System.enqueueJob(job);
} else {
// Schedule for later, or notify, or just skip
}
}
public static void bulkUpdate(List<Account> accs) {
if (accs.size() > Limits.getLimitDmlRows() - Limits.getDmlRows()) {
// Defer the rest to a batch
Database.executeBatch(new AccountUpdateBatch(accs), 200);
} else {
update accs;
}
}
Limits.getQueries(), Limits.getDmlStatements(), Limits.getCpuTime(), and friends report the running totals. Always pair getX() with getLimitX() for the cap.
The one “recovery” pattern that does work: Publish Immediately platform events
Platform Events configured with Publish Behavior = Publish Immediately are sent outside the transaction’s commit. They survive a rollback. This is the pattern used by enterprise apps that want a guaranteed audit trail even when the originating transaction fails — including failures caused by governor limits.
try {
// risky work
} catch (Exception e) {
EventBus.publish(new System_Error__e(
Message__c = e.getMessage(),
Stack__c = e.getStackTraceString()
));
throw e; // re-throw to surface the failure
}
The event lands in a separate transaction handled by your async subscriber and writes the log.
What interviewers are really looking for
The naive answer is “yes you can catch it.” The senior answer says: yes, the type is catchable, but the transaction is dead — DML and callouts in the catch don’t survive. The right pattern is proactive Limits.getX() checks before the risky operation and Publish Immediately platform events when you genuinely need an audit row to survive failure. Mention that, and you’ve shown production triage experience.
Verified against: Apex Developer Guide — Exception Class and Built-In Exceptions, Platform Event Behavior. Last reviewed 2026-05-17.