… that is the question. Not really, the real question is:
When should I throw and when should I return?
If you are already working in an infrastructure of a large system, then you are at the mercy of that system… unless of course you are a System Architect and you are expected to change this 😉
When working in an existing system the basic rules are:
- If you can throw, stop the current users request, and safely return back to user prompting when throwing, then you should throw.
- If you are not guaranteed that a throw will notify the user of the problem and safely return them to a prompt, then you need to return so that you are providing this feedback to the user before the system takes over.
Most of the debate on this topic stems from system design, and if you are laying out a new Framework, then what is best?
Checked Exception Specifications
One major consideration I use when making this decision is:
When using the design model of “Return Type Indicates Failure”. Then the architecture of the system should handle ALL code, which will throw an exception, at the lowest low-level calls. Without this, you run the risk of missing a throw, which stands the risk of bringing the application down.
Albeit, that is the very purpose of an exception – an unknown condition occurred and we want the program to stop before it runs awry and maybe does something worse than just exiting. Sometimes this is good – to just exit. Most of the time though, we are expected to implement system recovery, and we shall not let the application die. Programs will throw, they always have, and they always will.
In languages like Java, methods that throw checked exceptions are very visible and you must either handle the exceptions from calls to these methods, or propagate the exception upwards. You can of course use unchecked exceptions in Java (i.e. Runtime Exceptions or Errors) which will also bring the application down.
In C++ you have the option of specifying the ‘throw’ as an Exception Specification, but this has several limitations and doesn’t really serve to solve any of the problems you might want to resolve by using it.
New System Architecture
Finally, we move into the meat of the discussion that you probably care about.
The basics are this, any good system architecture will be able to handle failures and recover from them. This includes exceptions, even if you are designing a system that will use return by failures. To design a system like this you must design in modules, who’s bounds are well establish so that in case of catastrophic failure, you can help the system to recover and understand what data can and what data cannot be trusted.
Any language worth developing in would make the basic guarantee that when an exception is thrown, and as the stack starts to unravel, destructors are called, things are cleaned as best the programmer instructed, and that no leaks occur. This may mean that some objects are in an unstable state, but that those objects should be able to be destroyed, or even used – even if their state is not predictable. Now I would not go off and start using objects that were being modified when an exception was thrown, but the point here is that you could and many times in production code you will see this done with try/catch blocks.
With this understanding we can make a pretty solid design principle, which would state something like:
A well designed system architecture, one whose modules are perfectly decoupled, and one which uses RAII as it should, would lead to modules being the perfect boundaries at which exception handling can be done to guaranteed system recovery in the face of catastrophic failure.
All this really means is that a caller should not know about the inter-workings of sub-modules (i.e. decoupling). If this practice is followed, and the modules do a good job of cleaning up after themselves, then if anywhere in that module an unrecoverable exception occurs, the caller can catch that exception, understand the full scope of the failure, handle it gracefully, and continue on without being concerned that it’s state or that it’s children’s state are unpredictable.
Note: Modules in this context are arbitrary and used to communicate the conceptual boundaries between parts of a systems design. They mean to communicate that the try/catch blocks of exceptions, which are used for system recovery, should not exist in objects which are being modified, but at well defined boundaries (i.e. modules).
To extrapolate on this a little, and to drill at the answer you are looking for, a well designed system must use exception handling at the boundaries. Within the boundaries I’d say do what makes sense, sometimes using return types to make decisions about failures makes life easier and sometimes throwing and giving control back to the user as soon as is possible makes life easier.
Do understand that throwing exceptions should never be part of a design practice as they are very costly from a performance perspective and they rely on developers doing a good job of cleaning up, but in case of unexpected conditions, they sometimes are the only call that makes sense.
I like science and writing software.