AfterUpdate.ContinueOnError
Logs and swallows the exceptions of an after update Writer or Dispatcher, so later handlers still run and the update is saved. A Writer also gets a private unit of work, so a failure discards only its own writes.
Interface
apex
public class AfterUpdate {
public interface ContinueOnError {
}
}AfterUpdate.ContinueOnError has no methods: implementing it is the whole opt-in.
Example
apex
public with sharing class ContactWriter implements AfterUpdate.Writer, AfterUpdate.ContinueOnError {
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChanged(Contact.AccountId);
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
unitOfWork.toInsert(new Task(WhatId = ((Contact) record.getNewSObject()).AccountId, Subject = 'Review contact'));
}
}cls
public with sharing class OpportunityWinTaskWriter implements AfterUpdate.Writer, AfterUpdate.ParentQuery, AfterUpdate.ContinueOnError {
public Map<SObjectField, TriggerHandler.ParentFields> queryParentsOnAfterUpdate() {
return new Map<SObjectField, TriggerHandler.ParentFields>{ Opportunity.AccountId => TriggerHandler.ParentFields.with(Account.Name) };
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isChangedTo(Opportunity.StageName, 'Closed Won');
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Opportunity newOpportunity = (Opportunity) record.getNewSObject();
Account parentAccount = (Account) record.getNewParent('Account');
unitOfWork.toInsert(
new Task(
WhatId = record.getId(),
OwnerId = newOpportunity.OwnerId,
Subject = 'Onboarding kick-off: ' + (parentAccount?.Name ?? newOpportunity.Name),
Description = 'Won on ' + Date.today().format() + '. Agree the implementation plan with the customer.',
ActivityDate = Date.today().addDays(7),
Priority = 'High',
Status = 'Not Started'
)
);
}
}cls
public with sharing class AccountAddressCascadeWriter implements AfterUpdate.Writer, AfterUpdate.RelatedQuery, AfterUpdate.ContinueOnError, AfterUpdate.RecursionGuard {
public Integer maxRecursionDepthOnAfterUpdate() {
return 1;
}
public Map<String, AfterUpdate.RecordsProvider> queryRelatedOnAfterUpdate() {
return new Map<String, AfterUpdate.RecordsProvider>{ 'contacts' => new AccountContactsProvider() };
}
public Boolean writeOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
return record.isAnyChanged(Account.BillingStreet, Account.BillingCity, Account.BillingState, Account.BillingPostalCode, Account.BillingCountry);
}
public void writeOnAfterUpdate(TriggerHandler.UpdateRecord record, TriggerHandler.UnitOfWork unitOfWork) {
Account newAddress = (Account) record.getNewSObject();
Account previousAddress = (Account) record.getOldSObject();
for (SObject relatedRecord : record.getRelated('contacts').getAllWhereKeyEquals(record.getId())) {
Contact contactRecord = (Contact) relatedRecord;
if (!this.mailingFollowedAccount(contactRecord, previousAddress)) {
continue;
}
unitOfWork.toUpdate(
new Contact(
Id = contactRecord.Id,
MailingStreet = newAddress.BillingStreet,
MailingCity = newAddress.BillingCity,
MailingState = newAddress.BillingState,
MailingPostalCode = newAddress.BillingPostalCode,
MailingCountry = newAddress.BillingCountry
)
);
}
}
private Boolean mailingFollowedAccount(Contact contactRecord, Account previousAddress) {
if (String.isBlank(contactRecord.MailingStreet) && String.isBlank(contactRecord.MailingCity)) {
return true;
}
return contactRecord.MailingStreet == previousAddress.BillingStreet &&
contactRecord.MailingCity == previousAddress.BillingCity &&
contactRecord.MailingPostalCode == previousAddress.BillingPostalCode &&
contactRecord.MailingCountry == previousAddress.BillingCountry;
}
private with sharing class AccountContactsProvider implements AfterUpdate.RecordsProvider {
public List<SObject> query(TriggerHandler.UpdateRecords records) {
return [
SELECT Id, AccountId, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry
FROM Contact
WHERE AccountId IN :records.getIds()
];
}
public String keyOf(SObject record) {
return ((Contact) record).AccountId;
}
}
}Good to Know
- The handler stops. After an exception, its remaining records and its Finalizer are skipped. Later handlers still run.
- The private unit commits early. It commits right after the Writer, before the unit of work the other Writers share. A Writer that also implements OwnUnitOfWork gets its own unit instead.
- A Dispatcher keeps what it did. A job already enqueued or an event already published stays.
- Some errors still fail the update.
TriggerHandler.TriggerHandlerException,System.LimitExceptionand theFinalExceptionfrom writing to a trigger row are never swallowed. Neither are errors inbypassOnAfterUpdateWhen(), in the parent queries or in the commit after the last handler. - Add a Logger. Without a
TriggerOrchestrator.Loggerimplementation, a swallowed exception leaves no trace. See Errors & Logging.
