25

Reading Eric Lippert's article on exceptions was definitely an eye opener on how I should approach exceptions, both as the producer and as the consumer. However, I'm still struggling to define a guideline regarding how to avoid throwing vexing exceptions.

Specifically:

  • Suppose you have a Save method that can fail because a) Somebody else modified the record before you, or b) The value you're trying to create already exists. These conditions are to be expected and not exceptional, so instead of throwing an exception you decide to create a Try version of your method, TrySave, which returns a boolean indicating if the save succeeded. But if it fails, how will the consumer know what was the problem? Or would it be best to return an enum indicating the result, kind of Ok/RecordAlreadyModified/ValueAlreadyExists? With integer.TryParse this problem doesn't exist, since there's only one reason the method can fail.
  • Is the previous example really a vexing situation? Or would throwing an exception in this case be the preferred way? I know that's how it's done in most libraries and frameworks, including the Entity framework.
  • How do you decide when to create a Try version of your method vs. providing some way to test beforehand if the method will work or not? I'm currently following these guidelines:
    • If there is the chance of a race condition, then create a Try version. This prevents the need for the consumer to catch an exogenous exception. For example, in the Save method described before.
    • If the method to test the condition pretty much would do all that the original method does, then create a Try version. For example, integer.TryParse().
    • In any other case, create a method to test the condition.
4
  • 1
    Your example of a save that may fail isn't really a terribly vexing exception. It's quite ordinary and probably should simply be an exception.
    – S.Lott
    Commented Feb 6, 2012 at 15:46
  • @S.Lott: What do you mean with it's quite ordinary? The situation itself, or throwing an exception in this situation? Anyway, I agree with you that it's not evident if this is in fact a vexing situation. I'll update the question.
    – Mike
    Commented Feb 6, 2012 at 16:21
  • "The situation itself, or throwing an exception in this situation" Both.
    – S.Lott
    Commented Feb 6, 2012 at 16:31
  • "With integer.TryParse this problem doesn't exist, since there's only one reason the method can fail." - Eh, depends on how you look at it. Is "there's non-digits in the string" and "it's an integer, but it's too large to fit an int" the same problem? The real question is, does the caller care about the reason? Commented Mar 16, 2021 at 8:30

4 Answers 4

28

Suppose you have a Save method that can fail because a) Somebody else modified the record before you, or b) The value you're trying to create already exists. These conditions are to be expected and not exceptional, so instead of throwing an exception you decide to create a Try version of your method, TrySave, which returns a boolean indicating if the save succeeded. But if it fails, how will the consumer know what was the problem?

Good question.

The first question that comes to my mind is: if the data is already there then in what sense did the save fail? It sure sounds like it succeeded to me. But let's assume for the sake of argument that you really do have many different reasons why an operation can fail.

The second question that comes to my mind is: is the information you wish to return to the user actionable? That is, are they going to make some decision based on that information?

When the "check engine" light comes on, I open up the hood, verify that there is an engine in my car that is not on fire, and take it to the garage. Of course at the garage they have all kinds of special purpose diagnostic equipment that tells them why the check engine light is on, but from my perspective, the warning system is well designed. I do not care whether the problem is because the oxygen sensor is recording an abnormal level of oxygen in the combustion chamber, or because the idle speed detector is unplugged, or whatever. I'm going to take the same action, namely, let someone else figure this out.

Does the caller care why the save failed? Are they going to do anything about it, other than either give up or try again?

Let's assume for the sake of argument that the caller really is going to take different actions depending on the reason why the operation failed.

The third question that comes to mind is: is the failure mode exceptional? I think you might be confusing possible with unexceptional. I would think of two users attempting to modify the same record at the same time as an exceptional-but-possible situation, not a common situation.

Let's assume for the sake of argument that it is unexceptional.

The fourth question that comes to mind is: is there a way to reliably detect the bad situation ahead of time?

If the bad situation is in my "exogenous" bucket, then, no. There's no way to reliably say "did another user modify this record?" because they might modify it after you ask the question. The answer is stale as soon as it is produced.

The fifth question that comes to mind is: is there a way to design the API so that the bad situation can be prevented?

For example, you could make the "save" operation require two steps. Step one: acquire a lock on the record being modified. That operation either succeeds or fails and so can return a Boolean. The caller can then have a policy about how to deal with failure: wait a while and try again, give up, whatever. Step two: once the lock is acquired, do the save and release the lock. Now the save always succeeds and so there is no need to worry about any kind of error handling. If the save fails, that is truly exceptional.

4
  • All very good points, thanks. Now here's a rhetorical question that probably summarizes my post: If you were to redesign File.Open(), would you create a File.TryOpen() instead? How would you communicate the consumer the reason of the fail? Or is throwing an exogenous exception really the best compromise here?
    – Mike
    Commented Feb 6, 2012 at 17:02
  • 11
    @Mike: File systems are a good example of the use of exogenous exceptions. They fail rarely, so failure is exceptional. They fail unpredictably and for reasons entirely outside of the control of the caller (there is no "lock" you can take that keeps the ethernet cable plugged in), and the failures are both diverse and actionable (that is, a failure because file is not found vs file is found but you don't have write access are both actionable in different ways.) All these are reasons to represent a failure as an exception. Commented Feb 6, 2012 at 17:21
  • I do consider the question to be answered now, but I'm just curious ;) If the method can fail for 2 or more reasons, the failures are actionable, the failures are unexceptional and the failures cannot be detected ahead of time nor prevented, what would you do?
    – Mike
    Commented Feb 6, 2012 at 19:50
  • @Mike I can't speak for Eric, but that sounds like a good place for error codes. Returning a member of an enum, perhaps. Commented Jun 2, 2012 at 20:26
1

In your example, if the ValueAlreadyExists situation can be easily checked, it should be checked and an exception could be raised before attempting the Save, I don't think a Try should be necessary in this situation. The race condition is harder to check ahead of time, so wrapping the Save in a Try in this case is probably a very good idea.

In general, if there's a condition that I think is very likely (such as NoDataReturned, DivideByZero, etc...) OR is very easy to check for (such as an empty collection or a NULL-value), I try to check for it ahead of time and deal with it before I ever get to the point where I'd have to catch an exception. I admit it's not always easy to know these conditions ahead of time, sometimes they only appear when the code is under rigourous testing.

0

The save() method must throw an exception.

The top-most layer should catch and inform the user, without terminating the program, unless it's an Unix like command line programs, in which case it's OK to terminate.

Return values are not a good way to manage exceptions.

1
  • Tulains, this is more and more becoming a minority opinion.
    – gnasher729
    Commented Mar 15, 2021 at 13:48
0

A “vexing exception” is an exception where the software supposed to handle it has no clue what to do. The “something went wrong somewhere” kind of exception. You ask how to avoid throwing them.

You throw an exception if your code can’t do what it is supposed to do, and in a way not covered by return values or status codes. What you can avoid is situations where bugs in your code prevent you from doing the job when it could be done. Say your code sorts a customer list, checks it, finds it is not sorted, and throws an exception. That is vexing and unnecessary.

Or you rely on other code that fails for no discernible reason. If you can’t work around that failure it may turn into a vexing exception.

Useful exceptions will tell the caller: 1. What you ask cannot be done. 2. It didn’t work, it might work later. 3. It didn’t work because of a bug in your code needs fixing. 4. It may work if you change your request. All things where the developer can take action. Try what you can to avoid exceptions not in these categories.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.