[SalesForce] When do we really need try/catch

I know about 'always in DML operations'. But for what? What do we get? The code will crash anyway, and we can find the error in logs even without this construction. What's the main point of try/catch? And what exactly can we do in catch except System.debug() or sending email alert with an error message? I would be grateful for the answers with examples.

Best Answer

What the main point of try/catch?

To catch and handle an exception. The handling is the key.

What it means to handle an exception is to take an exceptional situation - something bad and out of the ordinary happened - and allow the application to safely move back into an anticipated pathway of operation, preserving

  • The integrity of the data involved.
  • The experience of the user.
  • The outcome of the process, if possible.

Let's look at a couple of examples.

Triggers

You're writing a trigger. The trigger takes data modified by the user, processes it, and makes updates elsewhere. It performs DML and does not use, for example, Database.update(records, false), meaning that failures will throw an exception. (If you do use partial-success methods, the same principles apply, they just play out differently because errors come to you in Result objects instead of exceptions).

Here, you have to answer at least two critical questions:

  • Are the failures I encounter amenable to being fixed?
  • What is the nature of the data manipulation I'm doing? If it fails, does that mean that the entire operation (including the change the user made) is invalid? That is, if I allow the user changes to go through without the work I'm doing, have I harmed the integrity of the user data?

These questions determine how you'll respond to the exception.

If you know a particular exception can be thrown in a way that you can fix, your handler should just fix it and try again. That would be a genuine "handling" of the exception. However, in Apex, where exceptions aren't typically used as flow control, this situation is somewhat less common than in e.g. Python. That said, one example where I've personally implemented such a handler is in a Queueable that attempts to lock a record FOR UPDATE. In that situation, where I had a potential race condition to avoid, catching the QueryException when that query times out and simply re-enqueuing the Queueable to try again was the right pattern.

But in most cases, that's not your situation when building in Apex. It's the second question that tends to be determinant of the appropriate implementation pattern, and it's why I tend to eschew exception handlers in many cases.

The most important job your code has is not to damage the integrity of the user's data. So in most cases where an exception is related to data manipulation, I advocate for not catching it in backend code at all unless it can be meaningfully handled. Otherwise, let higher-level functionality (below) catch it, or allow the whole transaction to be rolled back to preserve data integrity.

So, again, to make this concrete: you're building a trigger whose job is to update the dollar value of an Opportunity when the user updates a related Payment. Your Opportunity update might throw a DmlException; what do you do?

Ask the questions: Can you fix the problem in Apex alone? No. If you let the Opportunity update fail while the Payment update succeeds, do you lose data integrity? Yes.

Let the exception be raised and dealt with, or allowed to cause a rollback, at a higher level.

Non-Critical Backend Functionality

But there are other cases where you'll want to catch, log, and suppress an exception. Take for example code that sends out emails in response to data changes (I'll save for another time why I think that's a terrible pattern). Again, look to the questions above:

  • Can I fix the problem? No.
  • Does the problem impact data integrity if I let it go forward? Also no.

So here is a situation where it might make sense to wrap the sending code in a try/catch block, and log email-related exceptions using a quality logging framework. Then, don't re-raise - consume the exception and allow the transaction to continue.

You may not want to block a Case update because some User in the system has a bad email address!

User-Facing Functionality

Now, turn the page to Lightning and Visualforce. Here, you're building in the controller layer, interpreting between user input and the database.

You present a button that allows the user to perform some complex operation that can throw multiple species of exceptions. What's your handling strategy?

Here, it is much more common, and even preferable, to use broad exception handlers that don't actually handle the exception, but perform a rollback to preserve data integrity and then surface a friendly error message to the user.

For example, in Visualforce, you might do something like this:

Database.Savepoint sp = Database.setSavepoint();
try {
    doSomeVeryComplexOperation(myInputData);
} catch (Exception e) { // Never otherwise catch `Exception`!
    Database.rollback(sp); // Preserve integrity of the database.
    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'An exception happened!'));
}

That's friendly to the user - it shows them that the higher-level, semantic operation they attempted failed (and you might want to include the actual failure too to give them a shot at fixing it) - and it's also friendly to the database, because you make sure your handling of the exception doesn't impact data integrity.

Even better would be to be specific about the failure using multiple catch blocks (where applicable):

Database.Savepoint sp = Database.setSavepoint();
try {
    doSomeVeryComplexOperation(myInputData);
} catch (DmlException e) { 
    Database.rollback(sp); // Preserve integrity of the database.
    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'Unable to save the data. The following error occurred: ' + e.getMessage()));
} catch (CalloutException e) {
    Database.rollback(sp); // Preserve integrity of the database.
    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.FATAL, 'We could not reach the remote system. Please try again in an hour.');
}

In Lightning (Aura) controllers, you'll re-throw an AuraHandledException instead.

The One Thing Not To Do

This:

try {
    // do stuff
} catch (Exception e) {
    System.debug(e);
}

There are very, very few situations where this is a good pattern. You almost never want to swallow and hide an exception, because no one is ever going to look at that log - and you'll have silent failures happening in your org, violating user trust and imbuing the system as a whole with inexplicable behavior.

If you don't need to take action on an exception, log it in a way that a user can review and act on. Never swallow it.

Think about how you'd have to answer the questions above to make this a good pattern. You'd want to swallow an exception like this when:

  • What you're doing has no impact on data integrity.
  • Nobody is going to notice or care that the functionality isn't working.

If those things are both the case, I suspect a design fault in the system!

This pattern also makes debugging excruciating, because your code fails silently when it should be screaming in the form of an exception.