How Salesforce Developers Handle Errors with Try, Catch, & Rollback Best Practices

Stop, Drop, and Rollback

Smokey The Bear Says — Care will prevent 9 out of 10 Apex errors!

Exceptions note errors that disrupt the normal flow of code execution. Try/catch blocks provide a strong defense in finding resolutions for these exceptions.

When an exception occurs, code execution stops and all Data Manipulation Language (DML) operations processed before the exception are rolled back without being committed to the database. The Salesforce user may see an error message in the Salesforce user interface when exceptions remain unhandled.

Saving partially processed code data in Salesforce

There are a few assumptions when coding in Salesforce with regard to protecting the developer from saving partially processed data in Salesforce.

  • Any exceptions that happen in code (within the same entire execution) will roll back the entire execution so all database changes leading up to the failure will not be committed to the database. This is true for triggered code, page controllers, asynchronous code, etc. This was a GREAT thing as I started developing in Salesforce and really protected the data for clients.

The above is only true if you are:

  1. Not using try/catch blocks around DML operations, which results in undesirable errors on top of standard pages or white-screen error pages from custom pages.

  2. Using try/catch blocks and are properly handling the error inside the 'catch.’

My experience with using try/catch blocks in Salesforce coding

The more custom work I did, the more I realized I was using try/catch all over the place but didn't immediately understand the implications of using it. I've talked about this topic many times with other developers to explain my experiences, and it can be a complex topic to discuss.

Here are some hypothetical situations to explain what I mean.

Hypothetical use cases for the try/catch blocks:

  1. You want to use try/catch in a page controller so you can show a friendly error on the screen for the user. There are two DML operations in the 'button click' action, and the error happens on the second DML. Tell the user there is an issue by adding a pagemessage.

    Did you do anything to undo the first DML, which was a success? Salesforce will not rollback that DML for you because you've essentially "caught" the error that would have done that.

    Result: Bad data. You need to roll back.

  2. You've got a trigger on a case that will roll-up custom information to the account. This important logic will keep the data in check. But, sometimes there is an issue when you run the DML Update to the accounts, so you wrap the DML in a try/catch and perform a system.debug to figure things out a bit better. You've deployed things like this to production since it is very rare.

    The only handling you've done is a debug and you have caught the error, so you have told Salesforce not to roll-back the changes to the original cases that fired the trigger. You'll end up with cases (newer changes) out of sync with the account (failed update) in your database.

    The solution is that you need to attach a ".adderror()" to the appropriate case records in the trigger, which will tell Salesforce to roll back changes and will show your custom, more descriptive error to the user (or to the calling code / external system). Result:

    Bad data. You need to .addError

Apex Controller Examples (Lightning, Aura, LWC, Visualforce)​

Good Example no try/catch

Standard exception handling, Salesforce handles database protection.

KEY: Do nothing extra (Good)

// New account
Account a = new Account(Name = 'New Account');
insert a;
// If DML failed, salesforce shows error and rolls-back the new record

// New Case linked to account
Case c = new Case(subject = 'I need help', accountId = a.id);
insert c;
// If DML failed, salesforce shows error and rolls-back the new record

// If we get this far in code, both DML's finished properly
system.debug('Success');

Bad Example with try/catch

Account will insert without the case.

Since we are using try/catch below, we are able to show the user a nicer error message with the ApexPages.addMessage method. BUT, we are now taking over the standard Salesforce pattern of "error and rollback.” We are also forgetting to roll back ourselves.

// New account
Account a = new Account(Name = 'New Account');
try{

    insert a;
    system.debug('Success');

} catch(dmlexception e){

  // If DML failed, ends up here
  system.debug('Error inserting account: ' + e);
  ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Could not insert account record. Error: ' + e));
}

// ************ Assume that the account was a success above, continue below ***********


// New Case linked to account
Case c = new Case(subject = 'I need help', accountId = a.id);
try{

    insert c;
    system.debug('Success');

} catch(dmlexception e){

  // ************ Assume the CASE insert FAILED ***********
  // If DML failed, ends up here
  system.debug('Error inserting case: ' + e);
  ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Could not insert Case record. Error: ' + e));

}

// If we get this far in code, something may have failed and been caught above, or everything worked.
system.debug('Potential Success');

Good Example with try/catch

If Case fails, Account insert will roll back.

KEY: Using Database Rollback (Best Practice)

// Record the place "in time" where we would ultimately like to return the state of the database to upon an error.
Savepoint sp = Database.setSavepoint(); // <<<<<<<<<<<<

// New account
Account a = new Account(Name = 'New Account');
try{

    insert a;
    system.debug('Success');

} catch(dmlexception e){

 // If DML failed, ends up here
 system.debug('Error inserting account: ' + e);
 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Could not insert account record. Error: ' + e));

 // Upon error roll back the database 
 // this example would not do anything since Account is first, and upon failure "a" would not be in the database anyway
 Database.rollback(sp); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<
}

// ************ Assume that the account was a success above, continue below ***********


// New Case linked to account
Case c = new Case(subject = 'I need help', accountId = a.id);
try{

    insert c;
    system.debug('Success');

} catch(dmlexception e){

 // ************ Assume the CASE insert FAILED ***********
 // If DML failed, ends up here
 system.debug('Error inserting case: ' + e);
 ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Could not insert Case record. Error: ' + e));

 // Upon error roll back the database
 // This line would "undo" the account insert above and once the user sees the error message on screen we can 
 // feel confident that the database is not soiled with invalid information. In this example we are preventing 
 // accounts without a case from being inserted on each attempt.
 Database.rollback(sp); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<

 // You should fix your sobject variables now, they may have invalid Ids, look for Part 2 of this blog article to explain.
 // You may also need to handle the pagereference / redirect conditionally here, typically just refresh the page to see errors which means returning null.

}

// If we get this far in code, something may have failed and been caught above, or everything worked.
system.debug('Potential Success, conditional refresh of page or redirect etc.');

Apex Trigger Code Examples

Good Example no try/catch

Standard exception handling, Salesforce handles database protection.

KEY: Do nothing extra (Good)

trigger CaseTrigger on Case (after insert) {
 
 system.debug('***CaseTrigger begin');

 // In this example trigger we are simply wanting to flag at the Account level
 // that the Account has a Case linked to it, which is a lookup field on Case
 // so we wouldn't be able to do a rollup count.
 // In reality we would also want to track deletes of Cases but that is skipped 
 // for demonstration purposes.
  
 // bulk map of accounts that we need to update
 map<Id, Account> accountsForUpdateMap = new map<Id, Account>();

 // find accounts to update
 for(Case c : trigger.new){
  // if this case is linked to an account and we've not already tagged this account
  if(c.AccountId != null && !accountsForUpdate.containsKey(c.AccountId))
   // add this account to the map
   accountsForUpdate.put(c.AccountId, new Account(Id = c.AccountId, HasACase__c = true));
 }

 // check to see if we need to perform DML
 if(!accountsForUpdate.isEmpty())
  // let's do the DML
  update accountsForUpdate.values();
 
 // If we get this far in code, the DML finished properly
 system.debug('Success');

 // If anything were to go wrong in the above DML, since we do NOT have a try/catch then the server
 // will automatically rollback the changes (In this instance the Cases in this trigger
 // would not be inserted into the database since the account DML update failed. This is usually 
 // desired so that the Account data is accurate.)
 // A complex error would show on the calling page or calling code that fired this trigger

}

Bad example with try/catch

Account update could fail, but we didn't stop the Cases from being inserted.

trigger CaseTrigger on Case (after insert) {
 
 system.debug('***CaseTrigger begin');

 // In this example trigger we are simply wanting to flag at the Account level
 // that the Account has a Case linked to it, which is a lookup field on Case
 // so we wouldn't be able to do a rollup count.
 // In reality we would also want to track deletes of Cases but that is skipped 
 // for demonstration purposes.
  
 // bulk map of accounts that we need to update
 map<Id, Account> accountsForUpdateMap = new map<Id, Account>();

 // find accounts to update
 for(Case c : trigger.new){
  // if this case is linked to an account and we've not already tagged this account
  if(c.AccountId != null && !accountsForUpdate.containsKey(c.AccountId))
   // add this account to the map
   accountsForUpdate.put(c.AccountId, new Account(Id = c.AccountId, HasACase__c = true));
 }

 // check to see if we need to perform DML
 if(!accountsForUpdate.isEmpty()){
  try{
   // let's do the DML
   update accountsForUpdate.values();
  } catch(dmlexception e){
   // ************ Assume the ACCOUNT update FAILED ***********
   // If DML failed, ends up here
   system.debug('Error updating account: ' + e);
  }

 }

 // If we get this far in code, something may have failed and been caught above, or everything worked.
 system.debug('Potential Success');

 // If anything were to go wrong in the above DML, since we do have a try/catch the server
 // will NOT automatically rollback the changes. (In this instance the Cases in this trigger
 // WOULD be inserted into the database. This is usually NOT desired since the 
 // Account data is inaccurate.)
 // NO error would show on the calling page or calling code that fired this trigger. 

}

Good example with try/catch

If Account update fails, we flag the Cases with an error which will show to user and prevent the Case inserts.

KEY: Use .addError() method (Best Practice)

trigger CaseTrigger on Case (after insert) {
 
 system.debug('***CaseTrigger begin');

 // In this example trigger we are simply wanting to flag at the Account level
 // that the Account has a Case linked to it, which is a lookup field on Case
 // so we wouldn't be able to do a rollup count.
 // In reality we would also want to track deletes of Cases but that is skipped 
 // for demonstration purposes.
  
 // bulk map of accounts that we need to update
 map<Id, Account> accountsForUpdateMap = new map<Id, Account>();

 // find accounts to update
 for(Case c : trigger.new){
  // if this case is linked to an account and we've not already tagged this account
  if(c.AccountId != null && !accountsForUpdate.containsKey(c.AccountId))
   // add this account to the map
   accountsForUpdate.put(c.AccountId, new Account(Id = c.AccountId, HasACase__c = true));
 }

 // check to see if we need to perform DML
 if(!accountsForUpdate.isEmpty()){
  try{
   // let's do the DML
   update accountsForUpdate.values();
  } catch(dmlexception e){
   // ************ Assume the ACCOUNT update FAILED ***********
   // If DML failed, ends up here
   system.debug('Error updating account: ' + e);

   // Let's force the database to roll-back by adding an error to these records just 
   // like salesforce would normally do.  Our error can be more descriptive though.
            
   // Build a map of Case trigger rows keyed on the Account Id   
   map<id, list<case>> accountIdCaseMap = new map<id, list<case>>();
   for(Case c : trigger.new){
    // if this case is linked to an account that failed
    if(c.AccountId != null)
     // if we already found this account and this is another case, add this 
     // case to that list
     if(accountIdCaseMap.containsKey(c.AccountId))
      accountIdCaseMap.get(c.AccountId).add(c);
     // Add this account id with a new list of Case
     else      
      accountIdCaseMap.put(c.AccountId, new list<case>(c));
   }

   // Loop through the errors and tag the proper cases   
   for ( Integer i = 0; i < e.getNumDml(); i++ ){
    // for this dml error, get the id of the failed account, tag the matching cases.
    for(Case c : accountIdCaseMap.get(e.getDmlId( i )))
     // add an error to this row, which will prevent this row from being updated
     c.addError('Unable to update linked Account record due to an error: ' + 
        e.getDmlMessage( i ));
   }
       
  }

 }

 // If we get this far in code, something may have failed and been caught above, or everything worked.
 system.debug('Potential Success');

 // If anything were to go wrong in the above DML, since we do have a try/catch the server
 // will NOT automatically rollback the changes. But since we have used the .addError method on the 
 // correct trigger rows we have effectively prevented the case inserts.
 // Our error would show to the user on the standard page or to the calling code that fired the trigger.

}

Best Practices for Using Try/Catch Blocks in Apex

Generic exception class

When using the generic exception class, it will catch any type of error besides limits and some other situations. Be careful not to put too much inside the same try block because it'll be more difficult to figure out what caused the error. The best practice would be to use specific exception classes like dmlexception and only put a single DML in that try block.

Testing your catch blocks

Make sure to test your catch blocks. In your manual tests you can configure a dummy "requirement" on an object or a validation rule you know will fail, or omit a required field. Then, attempt the custom button, standard button, or whatever it is that starts the code that should fail, and see how your catch block operates.

This could be more difficult for unit tests, but you could employ some special logic to conditionally fill in data or fail to fill in data in your code.

Say your controller would be looking for "Unit Test *** Fail DML Required Field." It could know to purposely clear a field on a record before the DML, which would then fire your catch block. Or, you have a unique field setup in an object, and you let your unit test try to insert two records with the same values in that field. There are many other ways to test, but it’s up to you!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值