21

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.

4
  • 3
    try-catch isn't for developers but for end users. It's so us developers can provide user-friendly messages they can understand. Errors like NullPointerException: ... is almost unreadable to the normal user. Feb 24, 2020 at 19:01
  • 2
    I've retracted my close vote & I encourage others to do so as well - this turned into a very good discussion on the use of try/catch. Feb 24, 2020 at 21:48
  • @battery.cord I didn't vtc but I would imagine it's still off topic as the answer to this has been discussed many times on stack overflow and maybe a few other SE sites as well. The fact that it's try-catch on SF changes nothing.
    – Aequitas
    Feb 25, 2020 at 4:55
  • 1
    That's "crash" not "crush."
    – JRE
    Feb 26, 2020 at 10:36

5 Answers 5

46

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.

7
  • 5
    this is a very good answer
    – cropredy
    Feb 24, 2020 at 19:56
  • 1
    Great to see best practices in different SF contexts :bow: Feb 24, 2020 at 19:59
  • @DavidReed What would be the best approach to handling a post-install script exception within a managed package? It seems like something that we'd like to catch so that it doesn't prevent the installation altogether (post-install is not mission-critical and manual post-install steps could be used), but I'm also hesitant to do any email sending commands just in case those email settings are turned off (like the default settings in a sandbox...) Feb 24, 2020 at 21:38
  • 1
    @BrianMiller That is a great question, and to be honest I don't think I've written enough post-install scripts (1? maybe?) to address it properly.
    – David Reed
    Feb 24, 2020 at 21:49
  • 1
    @BrianMiller As a customer I'd rather see the install fail with some kind of actionable message than have a package installed in an invalid state. Feb 24, 2020 at 21:56
18

always in DML operations

That's a common misconception. If you're using partial saves (e.g. Database.insert(records, false);), there's never a time where you need try-catch, as any exception is gracefully transformed into something your code can handle (except LimitException, which would kill the transaction uncontrollably anyways).

You should catch DmlException if not using partial saves. When that occurs, what you want to do is to display an appropriate error to the user, and optionally log the error/send an email/etc. You definitely should not use try-catch if the only thing you're going to is a System.debug, as that hides errors and creates problems in the long term.

I actually subscribe to the "as few try-catch blocks as possible" philosophy. If you don't have a reason to try-catch, don't try-catch. This is context sensitive. In a trigger, you should always be using partial saves and rollbacks. In a controller, you should try-catch to prevent blowing away the user's data entry, if not using partial saves. In an Aura controller, you should use try-catch to convert to an AuraHandledException (optional, if you simply use partial updates to begin with).

It is possible to write code for almost of your business processes without ever using a single try-catch. They should be the exception, not the rule. If you're not sure if you can get an exception, experiment. You'll come to learn when it is necessary (usually, because the docs explain that an exception is definitely thrown).

6

It seems you have some misconceptions about exceptions.

  • I don't know if I'd say that all DML operations should be inside of a try block
  • Exceptions can contain useful information for debugging, but if you "swallow" the exception (have a catch block that does nothing), then it makes debugging harder
  • An exception isn't necessarily the end of your execution. You can potentially recover and continue

Like anything, exceptions are a tool that's meant to be used for specific situations.

On exceptions

Exception handling isn't a concept limited to Salesforce. In Software Engineering curriculum, exceptions are presented as a "graceful failure" mechanism. Instead of a hard failure (i.e. your program is dead, and your computer might now be unresponsive now too. Just accept it.), it gives you a chance to clean things up (roll back a transaction, log a more useful message, etc...) or potentially recover and continue.

Probably the most common example is handling division by 0. Let's say you're calculating the average unit price of all line items on an Opportunity. If you have no line items, you might run into a piece of code that evaluates to 0/0. In this context, we can probably say that if there are no line items, the average price should be 0 and we can continue executing the rest of the code.

There are better ways to handle that particular situation (using exceptions in place of flow control statements like if/else, for, while, etc... is bad practice), but it illustrates the point.

Another use I've found is to prevent internal details from leaking to a third party application (well, most of the time, anyway). There's nothing this third-party system can do with a Salesforce exception, so I capture it and try to provide a more reasonable error (e.g. "Couldn't find this id in Salesforce, are you sure you sent it to us before now?")

Salesforce specifics

The wrinkle that Salesforce adds here is that some exceptions are specifically meant to be un-catchable as a means of preventing bad code from one customer from affecting other customers (by using an disproportionate amount of computational resources).

I don't think there's any restriction on the code that can be run in a catch block. Logging and other notification are the most common cases, but there are other well-established uses like handling partial DML failure.

0

A very common use case is loops that must continue even if 1 item throws an error

Example in peusdo cose:

for each user of users {
    try {
        send mail to user
    } catch (Exception e) {
        do whatever you want with the error
    }
}

For 10 users, if you had the same code without the try/catch and it throws an error at user index 3, then users at index 4 to 9 would not receive their email.

1
  • 2
    I think it's worth noting that for operations like upserting or sending emails, both methods have an "All or Nothing" parameter that would be a better solution for a case like this. Catching exceptions is a costly operation, so trying and catching in a for-loop like this is not something you would want to do for large datasets.
    – nbrown
    Feb 25, 2020 at 15:38
0

try / catch should be used high in the code, to abort the reasonable unit of the application, so that other similar units could still be processed.

  • For a web server, this is most often a server request.
  • For GUI, this is a response to a single user action, such as a keystroke or menu selection.
  • For command like application, it is often the whole application that can be terminated.

Do not catch the exception somewhere deeply in the code where you do not know what to do with it, and do not attempt to "fix" the case without knowing that actually happened. All results of the action must be reverted when exception has been thrown (authorizations denied, transactions rolled back, etc).

The error handling code seldom cares about what exactly failed - simply log and abort. Java had a complex hierarchy of exceptions with declarations on who may throw what, probably assuming that different exceptions will be handled in some very individual ways. Differently, C# does not even have throws keyword, you can throw any exception from anywhere. Finally, Google Go only has one exception for everything (panic) and this seems enough. It still can be caught.

A code that does not use exceptions just has a lot of return statements instead, breaking out of deeply nested call in a more difficult way.

1
  • 3
    Welcome to SFSE. I think this answer would be improved by specific application to the Salesforce platform and the Apex programming language. Right now, it's not clear how to apply this answer to work on the Salesforce platform.
    – David Reed
    Feb 25, 2020 at 17:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .