0

Whether error handling by throwing exceptions is good or bad is contentious.

Are exceptions as control flow considered a serious antipattern? If so, Why?

The common line is that exceptions are for "exceptional circumstances". But what if a library author has decided to throw an exception? The library author cannot predict what might be an "exceptional circumstance" for us.

Of course, if we cannot rule out an exception being thrown from a function that we must use as-is and permitting the exception to bubbling up to the top is unacceptable then we must catch the exception.

Often however, we can take preventative measures. With sufficient effort we might even be able to guarantee that the conditions under which the library function would throw an exception will never occur. The throw will still be there (it's not our choice) but we can eliminate the need for try and catch.

An example is std::map.at(). (Pretend there's no std::map.find().)

What is the general wisdom? Is it in good taste to write code with the expectation that a library function will occasionally throw? Is client code obligated to minimize the use of exceptions?

Edit To try and distinguish this more from past questions and be more concrete, suppose we have some library function that might throw a file not found exception. Some client code wants to use the library to access a file. The client code is being programmed with the expectation that the file sometimes will not exist, even under "unexceptional circumstances". The client code can be made able to check if the library function will throw an exception before being called but it's non-trivial to do so. Should client code pile additional error checking on top of the error checking within the library or should the client code use the library generated exception as control flow?

In such a case adding error handling code on top of the library will almost certainly be less efficient than catching an error, will require less coding, and it could be argued that you must have a try/catch anyway. On the other hand it is a decidedly unexceptional control flow mechanism.

6
  • 2
    Isn't this 'done to death' on the web? In the question you linked to, there is an answer which summarises the issues described at c2.com/cgi/wiki?DontUseExceptionsForFlowControl. So I don't understand the nuance that you might be asking that hasn't already been answered. Can you give a 'unlike those questions and answers, I am asking ...' to help me understand?
    – gbulmer
    Aug 14, 2014 at 21:34
  • The difference is that the exception already exists and is for whatever reason set in stone. Should client code be responsible to avoid incurring exceptions? The other questions I can find regard whether or not the exception should have been put there in the first place. Aug 14, 2014 at 21:44
  • 3
    see: How should I handle exception that *should* never be thrown? "let the application crash. If that ever happens, then you know that something is deeply wrong... It is best to not handle an exception if you cannot handle it intelligently..."
    – gnat
    Aug 14, 2014 at 21:44
  • 1
    Huh? If you didn't want an unhandled exception you probably don't want to immediately abort either. Aug 14, 2014 at 21:52
  • 1

6 Answers 6

6

The common line is that exceptions are for "exceptional circumstances".

Well, it's wrong.

Catching is certainly something you might want to do rarely, but exception throwing is not for exceptional circumstances at all. You should throw whenever a run-time condition means that you cannot fulfill your contract.

There's a big middle ground between "Using exceptions to replace if" and "Exceptions are only for exceptional circumstances".

Oh, and contracts that mean "Every caller must check for error every time", like return codes, are vastly worse than "Using exceptions to replace if".

10
  • 7
    Code not being able to fulfill its contract is an exceptional circumstance.
    – DougM
    Aug 14, 2014 at 22:20
  • 3
    It's perfectly non-exceptional to imagine that a file might not exist.
    – DeadMG
    Aug 14, 2014 at 22:21
  • 3
    you're using a weird definition of "exceptional." The word is closer to "unusual" than "unimaginable."
    – DougM
    Aug 15, 2014 at 0:35
  • 1
    I have always understood "exceptional circumstances" to mean "circumstances that are unlikely to occur if a normal user uses the system in a normal way, but that can occur anyway if the system is used in unusual circumstances or if unexpected interactions with external systems occur." Trying to work with a file that doesn't exist counts as an unusual way to work with a system in my book unless you wanted to create that file. Aug 15, 2014 at 7:08
  • 1
    @BartvanIngenSchenau: Seems perfectly reasonable to me that the user made a mistake and tried to work with a file that he thought he created but got put in the wrong directory or he accidentally deleted or something.
    – DeadMG
    Aug 15, 2014 at 9:28
4

Handle exceptions you can handle and let others bubble to the top.

When a library-function throws a FileNotFound exception and you have a way to gently handle that exception. Do so, but the same function might also throw a IAmGoingToCorruptYourMemoryInEvilWays exception which you can't handle then you let it bubble to the top.

The common line is that exceptions are for "exceptional circumstances". But what if a library author has decided to throw an exception? The library author cannot predict what might be an "exceptional circumstance" for us.

In my opinion you should read it as exceptional circumstances within a system. And for the library function the system is small and the act of trying to read a file that isn't there is exceptional. In the larger system where the library function is used, the file not being there may very well be something that's to be expected, so you deal with the FileNotFound exception in that system.

2

What is the general wisdom? Is it in good taste to write code with the expectation that a library function will occasionally throw?

I think you do not understand how the exceptions are used.

What is important to understand is : you can not ignore exceptions. In a normal program execution, an exception must not occur.

If it must not occur, then why use exceptions?

  1. to catch a bug - someone implemented something, that collides with other part of the software. Maybe set a constant wrongly.
  2. to catch invalid parameters - if the user enters wrong value, which is not checked, what do you do? Maybe something out of range, or string instead of a number.

In such a case adding error handling code on top of the library will almost certainly be less efficient than catching an error, will require less coding, and it could be argued that you must have a try/catch anyway.

Again, proves that it comes from a person not knowing what exceptions are.

Exceptions are thrown at places where the error occurs, and caught at places where you know how to deal with the error. This separates the program execution into two :

  • normal execution path
  • error propagation path - when an error happens

This way you can concentrate on the normal execution of the program, ignoring the error path. When an error happens, throw an exception, and move on.

Since, you do not need to check error code, your program will be smaller, more efficient, and easier to maintain and unit test. Last two items are specially good for developers.

Performance wise, program with the use of exceptions will be better or same then the program without exceptions, because of zero-cost mechanism. Only thing that is paid, is a bit increase in the program/library size increase.

Of course, throwing an exception is expensive, but it doesn't really matter. If the program can not continue, then it doesn't matter how long it takes to resolve the error. On the other hand, with error codes, you pay for every check.

1

To me it's all a matter of abstraction layer consistency: if you throw something you catch yourself in the same context you are probably abusing. If you are throwing because of something you -at your actual level- don't know how to handle, throwing is the right way to go.

Who called you probably known WHY did he called, and hence knows also what to do if something goes wrong.

Now, about the catch: the correct place to catch is the one at with the reason for the error can be understood and the problem can gently be solved.

Suppose you are writing a function to manipulate a file: if the file is not there or if you find it is not in the expected format, since it was not you who supplied the file, you cannot know why it happened. So, instead to attempt a workaround yourself (that can be not what your caller intended) just throw reporting the problem.

Suppose you are he one that calls a function that has to manipulate a file: if that was you who gets the name of the file from the user, and the file is found as being not there or not being as expected, you should catch and ask again and eventually admitting an answer like "do nothing and let me go". If the name of the file was instead already given to you from somewhere upside, you should let the exception go up.

Another typical case is a function requiring a container to grow. If the memory allocation fails, the allocator will throw. The container- at that point- has to react to put itself in consistent state (no dangling pointers or half-made internal structures), but has to re-throw since it cannot solve the problem of the missing memory.

The catch should probably happen in a place when some other memory can be dismissed (an the operation re-attempted), or the user be informed of the problem to give it a chance to gain more memory for the actual task (for example by closing some other task or application) and reattempt the failed command.

1

Exceptions should be for exceptional control paths, meaning, unless there is an external error beyond the program's control (users jamming an abort button, files being corrupt that shouldn't be corrupt, etc), it should operate through the non-exceptional paths. Zero-cost EH actually cements this idea further in stone by making exceptional paths extremely expensive in exchange for making non-exceptional paths practically free.

However, a library implementer actually shapes the client's idea of what should not happen in a regular control flow. If a library function to open a file can throw a FileNotFound error, then that means the client should not attempt, under any cases they can control, to open files that aren't found. They shouldn't be trying to open files left and right willy-nilly and relying on the exception-handling system to discover what ones exist and what do not.

That said, the library implementer also has a responsibility in this case. Since they decided that opening a file that doesn't exist is an external input error which throws an enormously expensive exception, and therefore not something that should ever happen in ordinary circumstances, I would say it is also their responsibility to make sure the client can easily avoid making such an attempt through the normal, non-exceptional control paths.

So the library might provide, say, a can_open method that is intended to be called prior to attempting to use the open method (unless the client himself requires the file to be there to operate normally, at which point they can just call open directly), and the fact that the open method can throw would be reserved for those very extraordinary circumstances where the file might be deleted between checking to see if we can_open it and then trying to open it. These should be very exceptional circumstances.

Library Design

From the library designer's perspective, whether an open function should throw or return a success/failure status is going to be based on anticipated usage patterns.

Scenario A: If you anticipate that your library will be used in cases where it's a very common occurrence for a file being requested to be opened to not exist, returning a status code makes much more sense as opposed to throwing since a file not existing would be a common occurrence.

Scenario B: If you anticipate that your library will be used by people who often require a specific file to be there to operate normally, like a video game requiring a "game.dat" file to be there for it to run properly, then it makes more sense to design an open function which throws when the file isn't there and provide something like a can_open method for people in the Scenario A group above which would be the rare case demographic.

Here it's not a big deal either way and kind of a toss up, but try to design towards the common use case if you can anticipate it.

0

Java went much further and made Exceptions part of the method contract and, IMHO, that's good. It focuses your thinking about exceptions. When you call a method, you know exactly which exception(s) it might throw, because the method tells you what they are and the Java compiler insists that you deal with them - catch them and handle them, or change your method contract, declaring that "you" might throw these exceptions to your callers.

Because it doesn't explicitly do this, .Net has encouraged a lot of lazy, woolly thinking about exceptions, leading to all of the confusion already mentioned by others.

A method should throw an exception when it's asked to do something it can't.
Defensive programming can only give you so much protection - you can "pre-test" conditions as much as you like, but eventually you just have to call "that method" and you can still get "strange things" happening (system exceptions, race conditions, etc., etc.) between your last check and actually trying to do the work.

You should only catch an exception when your code can do something useful about it. Far too many Developers "finish" their programs and then ask about "Global" exception handlers - catching and logging the [whole!] exception to a file just before the application curls up and dies. OK, it's a backstop, but it's only "useful" up to a point. Catching that FileNotFoundException and trying to read a different file, all without the user knowing it's happened, is perhaps a little more "useful".

5
  • I prefer the .NET approach. When programming in Java, I found that my code ended up with hundreds of empty catch blocks, either because I knew that that exception will not occur, or because the documentation (not only external libs, but the Java class library too) gave absolutely no information on what that exception means. There are just too many places in official javadocs where you can read "This method throws an IOException if any IO errors occur". Thanks, very helpful.
    – marczellm
    Aug 15, 2014 at 16:38
  • Java checked exceptions missed one out of two awesome things about exceptions. Java started with checked exceptions and introduced unchecked, and this is now generally favored in most situations. Exceptions are awesome partly because mid level functions don't have to deal with them. Say f1() calls f2()... calls f20(), which is asked to open a file that doesn't exist. f2 through f19 can't do anything intelligent with this exception, so they ignore it. f1() can do a try catch to deal with it. This saves a crapload of cruft in your code... cruft that will be there if you use checked exceptions. Aug 16, 2014 at 8:11
  • @NirFriedman: You do not need exceptions to avoid cruft / boilerplate. E.g. Haskell offers a nice way to deal with error using special types like Maybe and Either and has a concise syntax that allows to specify only the happy path while errors are propagated implicitly. The difference is that with Maybe or Either you see that a function can fail by looking at its return type. With exceptions (especially unchecked ones) you never know what errors you can expect.
    – Giorgio
    Dec 10, 2017 at 21:13
  • @Giorgio In fact, you do. As soon as the possibility and type of failure is encoded in the signature, this means that adding errors to lower level functions may trigger a cascade of refactorings all along your call chain. This is exactly the thing I wrote that exceptions lelt you avoid. In my example above f2 through f19 would need to be changed also to propagate the error. Dec 11, 2017 at 3:41
  • @NirFriedman: I do not find it bad that you have to propagate the information about the possible errors to the types signatures throughout the chain of calls as long as you do not have to deal with them explicitly in the bodies of those functions.
    – Giorgio
    Dec 11, 2017 at 7:20

Your Answer

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

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