Sep 15, 2015

Stop Drop & Rollback -- How to handle errors properly in Apex/Salesforce with Try, Catch, Rollback Best Practices

Smokey Says - Care will prevent 9 out of 10 Apex errors!
There are some assumptions that we know about when coding in Salesforce in regards to protecting the developer from saving partial-processed data in Salesforce.

- Any exceptions that happen in code(within the same entire execution) will roll back the entire execution so that 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 the customers.

- The above is only true if: 1. You are 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. Or you are using try/catch blocks and are properly handling the error inside the 'catch'.

There are many uses of try/catch which I won't cover here but you can check that out here.

My Experience

The more custom work I did the more I realized that I was using try/catch all over the place but didn't immediately understand the dangerous implications of using it.  I've talked about this topic many times with other developers, explaining my experience, and it can be a complex one to discuss but I'll try to lay out some hypothetical situations.

Situations

  1. You want to use try/catch in a page controller so that you can show a friendly error on the screen for the user.  There are 2 DML operations in the 'button click' action, and say the error happens on the second DML.  So you tell the user there is an issue by adding a pagemessage.  Did you do anything to undo the 1st DML which was a success? Salesforce will not roll-back 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 Case that will roll-up some 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.  By "catching" this error and the only handling you've done is a debug, 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 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

Code Examples - VF Controller


VF Controller Good Example no try/catch - Standard exception handling, SF handles database protection KEY: Do nothing extra (Good)

VF Controller 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". And we are forgetting to rollback ourselves.


VF Controller Good Example with try/catch - If Case fails, Account insert will rollback KEY: Using Database Rollback (Best Practice)


Code Examples - Trigger

Trigger Good Example no try/catch - Standard exception handling, SF handles database protection KEY: Do nothing extra (Good)


Trigger Bad example with try/catch - Account update could fail but we didn't stop the Cases from being inserted


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)

Other Tips

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

- 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 leave out a required field. Then attempt the custom button, standard button, whatever it is that starts the code which should fail and see how your catch block operates.  For unit tests this could be more difficult 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 let your unit test try to insert 2 records with the same values in that field.  There's many other things you could do, up to you.