The Problem
Dealing with software errors (a.k.a. bugs, defects, flaws, failures) has unfortunately become part of normal, daily, modern life. Some errors result in international news stories – security breaches, privacy issues, and plane crashes. Others are less visible but cause data corruption, productivity loss, and low adoption rates.
Let’s start with a simple premise: reasonable customers, consultancies, and developers should agree that errors should be anticipated, planned for, and correctly handled.
An error may be introduced during analysis, design, or implementation of new code. During analysis, a business problem may be misunderstood or the wrong solution proposed. Design errors occur when factors such as reliability, scalability, or testability are ignored. Implementation errors are introduced when developers misunderstand requirements, write incorrect code, or fail to recognize potential failures. We’ll limit our discussion in this post to that last issue – failure recognition in Apex code.
Our Example
Before a failure can be ignored, it must be recognized. For lots of developers, this is a key step that needs some attention. To get a feel for where your error recognition skills are, let’s start with a quiz. The “myMethod” code below contains at least 13 potential errors. Can you find them?
public with sharing class MyClass {
public Id currentContactId;
public void myMethod(Contact person) {
if (person.Id == currentContactId) {
Account company = [SELECT Name FROM Account
WHERE Key__c = :person.Account.Key__c];
if (Integer.valueOf(company.Key__c) == 0) {
company.State__c = 'Processed';
update company;
}
}
}
}
Without any error handling code or comments, we have no idea whether the author recognized potential errors and thoughtfully considered them, or not. We might optimistically assume that if asked about error handling, the author would say, “Any exceptions that occur during this method will roll back the transaction and be displayed to the logged-in user. When run in an async context, an Apex exception will be emailed to administrators. I’ve written unit tests to verify that the method does exactly what it’s intended to do and handles all errors appropriately.”
But the more likely statement being made by the author of this code is this: “I did not take the time to look at the many errors this method might produce. I don’t know what failure will look like for various users in different contexts, or if the error handling will be acceptable to stakeholders. I wrote one unit test for coverage that executes just the happy path.”
Bad, right? Well… to be fair, there is little training and there are few incentives to encourage developers to write high-quality code. Error handling is rarely mentioned in Trailhead modules and Salesforce documentation. User story requirements usually don’t mention error conditions. Teams don’t discuss general approaches for a project. Code is typically not peer reviewed. Many errors are not found until the consulting team is paid and long gone. There are several big topics here that deserve their own, dedicated discussion. Another day…
Potential Errors in our Example
public void myMethod(Contact person) {
// 1. a more descriptive method name is needed
if (person.Id == currentContactId) {
// 2. exception if "person" is null
// 3. possibly bad logic & result if "currentContactId" is null
Account company = [SELECT Name FROM Account
WHERE Key__c = :person.Account.Key__c];
// 4. exception if current user does not own the record
// 5. exception if no record is found
// 6. exception if more than 1 record is returned
// 7. exception if this method is called many times - exceeding a limit
if (Integer.valueOf(company.Key__c) == 0) {
// 8. exception if "company.Key__c" is null
// 9. exception if "company.Key__c" is not an Integer
company.State__c = 'Processed';
update company;
// 10. exception if "State__c" is a restricted picklist
// and 'Processed' is invalid
// 11. exception if record has a blank required field
// 12. exception if validation rule fails
// 13. exception if any other DML failure occurs
}
}
}
How did you do? Did you think about Salesforce limits? Database errors that can be introduced by changing the data model after the code is tested? The importance of maintainability? Let’s go through the list above one-by-one and look at why each is important.
1. Method Name
public void myMethod(Contact person) {
// 1. a more descriptive method name is needed
Here are a few reasons you should care about something as seemingly trivial as method name:
- readability: naming conventions make code easier to read and understand, leading to better reviews and less time spent during debugging and enhancement
- focus: in determining a good name, you may realize a method needs to be more “focused”: have a single, clear, purpose. Good method design leads to reusability, testability, and all kinds of great outcomes.
2. Invalid Argument
if (person.Id == currentContactId) {
// 2. exception if "person" is null
This line makes the assumption that “person” is not null. Maybe that’s by design and maybe it’s not. If by design, it should probably be documented somehow.
Your team should have a policy for handling invalid method arguments. Your approach may depend on visibility.
3. Assumed Object State
if (person.Id == currentContactId) {
// 3. possibly bad logic & result if "currentContactId" is null
“currentContactId” may not have been set before this method was called. Assumptions of object state should be carefully considered. Avoid them, if possible.
4. Record Sharing
Account company = [SELECT Name FROM Account
WHERE Key__c = :person.Account.Key__c];
// 4. exception if current user does not own the record
Because this class is declared “with sharing”, the SOQL query will only return records that the running user has access to via ownership or some other sharing mechanism. No assumptions can be made about the “person.Account.Key__c”, since that value is passed in.
5. Query Exception
Account company = [SELECT Name FROM Account
WHERE Key__c = :person.Account.Key__c];
// 5. exception if no record is found
There are several flavors of errors that may result from a SOQL query. In this case, we’ll have a problem if no records are returned.
7. Exceeded Limit
Account company = [SELECT Name FROM Account
WHERE Key__c = :person.Account.Key__c];
// 7. exception if this method is called many times - exceeding a limit
If this method is called many times, it could contribute to a Salesforce limit being exceeded.
8. Passing a null argument
if (Integer.valueOf(company.Key__c) == 0) {
// 8. exception if "company.Key__c" is null
This is an example of the importance of understanding the methods you call. As a bonus challenge, Salesforce documentation doesn’t always indicate which exceptions a method might throw.
9. Be Careful With Data
if (Integer.valueOf(company.Key__c) == 0) {
// 9. exception if "company.Key__c" is not an Integer
How often have you heard a dismissive, “oh, that’s a data problem”? Well, a “data problem” usually means that the restrictions on the data model are insufficient. Do the attributes (required, unique, type, etc.) on a field need to be changed? Should validation rules be added? Even if the data model is rock solid, you may choose to handle such potential errors in your code – you never know what might change in the future…
10. More Data Issues
company.State__c = 'Processed';
update company;
// 10. exception if "State__c" is a restricted picklist
// and 'Processed' is invalid
11, 12. Data Model Change
update company;
// 11. exception if record has a blank required field
// 12. exception if validation rule fails
This is a fun one. There may be records in the system that can no longer be saved. Maybe they were created prior to the addition of a required field or validation rule. For whatever reason, your code may fail to do a simple update on an existing record.
13. Other (Salesforce)
update company;
// 13. exception if any other DML failure occurs
And of course… Salesforce reserves the right to throw you an exception at any time. Maybe your instance is down. Maybe the transaction is taking too long. If your code needs to be very robust, you’ll just need to always assume a database interaction (SOQL / DML) might throw an exception.
Apex BOOST is a free library available on the Salesforce AppExchange. Download it today to start asserting your assumptions, logging errors, and adding better diagnostic code to improve your custom code quality!
Takeaways
Thirteen errors in 10 lines of code?! This may seem overly-nitpicky or overwhelming. I agree that some types of errors are more serious than others. The likelihood of some error occurring might be so small that your team decides to ignore it. But I hope you agree that it’s good to be aware of all the ways your code might fail.
There are perfectly good reasons for ignoring any and all types of errors. But approach the analysis exercise honestly and carefully. Schedule pressure is almost never a good reason to ignore errors. Neither is “that should never happen” or “bad data.” With a little practice, you’ll become proficient at recognizing and handling errors and the resulting improvements in code quality will be well worth the effort.
We’ll be back soon to discuss the various ways these errors can be handled in your Apex code!