Java has had checked exceptions (ones that must be declared) since its very early days. Java’s initial design was generally not a “ground-breaking” project, mostly adopting patterns from existing languages. However its concept of checked exceptions was new.
There is plenty of debate on the internet about whether having checked exceptions in Java was a bad idea. At my current workplace, one colleague in particular has claimed “it has already been decided that checked exceptions are bad”.
I don’t agree, and here present a case in favour of checked exceptions. This article assumes you’re familiar with Java and its exception-handling abilities.
Clearly good software can be built in languages which support exceptions but don’t support checked exceptions. I’m not arguing for modifying Kotlin or C#, just arguing that:
- checked exceptions do have a number of positive aspects, and
- proper error-handling of exceptions (whether checked or unchecked) requires applying appropriate design patterns.
Note that while I have worked on software components of all sizes, the majority of my work has been on libraries, and on framework-like code at the center of large applications. This of course biases my personal preferences towards highly-reliable, highly-maintainable software, hard-to-misuse APIs, and (preferably) self-documenting code.
First, let’s look at how some other languages do this.
The classic “C” programming language doesn’t have exceptions; functions which can fail must return a status-code. As returning multiple objects from a function isn’t supported, three different strategies are used:
- function returns status, with actual computed values returned via “output parameters” - somewhat clumsy, but it works.
- function returns the computed value, with status returned via an “output parameter” or via the static “errno” variable or similar - also clumsy.
- when the (successful) result has some subset of values that are not “valid”, these invalid entries are used to indicate function status - eg functions which return an int which is (on success) never negative can use negative numbers to indicate failures. Such “combined” output values is efficient but really ugly and somewhat error-prone.
One problem with the lack of exceptions in C is that some problems are so rare that in practice almost no application handles them correctly - eg out-of-memory. Functions that directly allocate memory get an appropriate error-code, but to propagate that problem up to the caller in a sensible manner is difficult. Applications that must be truly robust (eg control systems) will invest the effort to do this properly, but most other applications (and even libraries) just don’t bother. Exceptions are quite useful for such rare-and-serious conditions.
Rust is one of the cool new kids on the block. Rust’s error handling primarily involves functions which can fail using a Result instance as its return-type - ie an
(error-object, output-object) pair. This is relatively similar to C’s approach but due to the use of a dedicated type, it is far harder for code to “forget to handle errors” than in C. Rust also provides a convenient syntax (the “?” operator) for passing an error from a called function up to the caller - which in some ways is what an exception is doing. Like exceptions, Rust’s “error objects” are also proper types
Rust does also have a kind of exception - the
panic keyword. Unlike Java, however, it is meant only for very serious situations - something like Java’s “assert”. There is a compile-time option (panic=abort) which causes the application to just terminate immediately if a panic occurs. The alternative behaviour is to do “stack unwinding” (as with Java exceptions) which allows destructors in various objects to be called appropriately. And it is possible to “catch” a panic, but this a fairly rare thing to do as the vast majority of functions report errors via return-values not panics. A panic condition therefore indicates something pretty serious has happened, from which recovery is not likely to be trivial. Of course, panics are not “checked”, because they are so rare. Note that while function
panic takes only a string-parameter, function
panic_any can accept an arbitrary object which can be retrieved higher in the stack - ie somewhat “exception-like”. Using this extensively isn’t “good Rust practice” though.
Is Go still one of the “cool new kids”? Regardless, it is newer than most widely-spread languages. Go does have “panics” - ie unchecked exceptions that are really only for truly exceptional circumstances, and it is possible to “recover from” a panic. However in general Go functions are expected to return
(error-object, output-object) tuples.
The C++ language does support exceptions, and they are always unchecked - ie there is no language syntax to indicate whether a function throws an exception. However given its heritance from C, many standard functions return error-codes rather than throwing exceptions; I think it’s fair to say that C++ code uses exceptions far less than Java code. While I have worked often in C++ in the past, it is not recent enough for me to be able to say whether “typical” C++ error-handling style is closer to Rust, or Java.
Kotlin has exceptions, and they are unchecked.
The Kotlin standard library does not include classes for the functional-language-centric error handling approach (Either/Maybe/etc), and therefore many Kotlin libraries do throw unchecked exceptions. Given Kotlin’s good support for functional programming principles, however, there are many libraries and examples of error-handling without exceptions, eg:
- Do You Even Try?
- Typesafe Error Handling in Kotlin
- KEEP Kotlin stdlib proposal for success-or-failure - with a great long discussion of principles and problems of error-handling in Kotlin
- DoUEvenCode: Kotlin Error Handling
I certainly wouldn’t recommend fighting against a language’s design principles. However it might be worth trying to adopt a more returned-result-centric programming style (ie traditional functional programming style) than continuing to use exceptions as Java does while just eliminating the checks (compile-time support).
I am particularly concerned about arguments that “checked exceptions get in the way of using anonymous functions/lambdas”; I address this later but in summary: a checked exception is telling the caller about a possible error that can be reported. Most errors should be handled (excluding “exceptional circumstances”), and IMO a compiler should remind developers to do so.
Haskell does have exceptions, and they are unchecked. However “catching” an exception requires using the IO monad which effectively forces exception-handling to be done at a “high level” in the application. Exceptions therefore cannot be used for “local flow control” as they are in Java; instead functions return
(error, output) pairs in the typical functional-language style.
Errors are caused by one of three things:
- the input-parameters to a function,
- the application state, or
- the environment (including external processes and remote services)
Only the first kind of error can occur in a pure function; they do not reference state or environment. Problems such as assertion failures can occur, but these are classic “unchecked exceptions” that should only be handled in very high-level code (which is almost always already an IO monad), if at all.
And in Haskell, the second two points are covered by the IO monad. It therefore makes sense for functions to return errors, not throw exceptions, and for all exception-handling (eg problems related to missing files or uncontactable external services) to be done in an IO monad.
Python of course does have exceptions, and they are unchecked only - but that’s no surprise given Python’s dynamic type approach.
Scala runs on the JVM but has only unchecked exceptions. However its standard library includes standard functional types
Maybe as a way to return
(error, output) pairs and at least some sources recommend this over throwing exceptions.
C# has exceptions, and they are unchecked - as with Kotlin. I don’t use C# and so cannot say what is considered “best practice” for this environment.
Erlang and Elixir (two languages for the BEAM VM) have only unchecked exceptions. However as noted in the Elixir guide:
In practice, however, Elixir developers rarely use the try/rescue construct.
Functional Programming Principles and Exceptions
In functional programming, exceptions are not traditional; functions instead return
(error, output) pairs as an instance of a type named
Maybe, or similar. Some references include:
- Manning: Functional Programming in C#
- Belief-driven-design: Functional Programming with Java Exception Handling
- StackOverflow: How functional programming achieves “No runtime exceptions
Before we dive into other people’s opinions about Java’s checked and unchecked exceptions, I’d like to state mine first.
I don’t think there is any debate about using exceptions for truly exceptional cases such as out-of-memory. Scattering code to handle these kinds of problems throughout a code-base clutters it with little benefit. But Java code often uses exceptions for reporting errors that could reasonably be expected, eg FileNotFound; whether these should be checked or unchecked is the debate here.
I like the functional approach, as seen in Rust/Haskell/Scala/Go and functional languages in general (and also found in non-functional language C) - in general, failures should be returned from functions. Exceptions should be used only for truly exceptional situations, ie ones that are rare and difficult to recover from.
Callers of a function which can fail should be required to explicitly deal with that failure - which may mean reporting failure to the caller but with a different “context”.
However C’s “cram error-status into the return value in an ad-hoc way” is just not elegant, while its “return status in output parameter” approach is (a) not much nicer, and (b) doesn’t work well in Java which always passes parameters by-value rather than by-reference.
Because every object in Java is allocated on the heap, returning an object equivalent to Rust’s
Result<T, E> or type
Either (from multiple languages) produces garbage that needs to be collected later. Java’s lack of any decent syntax for dealing with such objects also makes this approach ugly in Java. Hopefully the current work on supporting value types and pattern matching in Java (see project Valhalla) should make it possible to return equivalents of these types and to unpack them efficiently in the calling code. Note however that despite the previously noted issues with Java and return-objects, Java now offers type
Regarding Java inefficiency when returning an error object: of course an exception is also an object on the heap. And even less efficient than a simple error-code as the stack-trace needs to be captured too. However they are only allocated on failure, while encoding status in return-objects requires an allocation even on successful calls.
One intended goal for exceptions was to separate the error-handling path in a function from the success-path. In practice, however, this doesn’t work so well; any recovery logic tends to depend upon which step in a sequence failed - which means catching after each function-call, or after small groups, rather than just having one catch. Even throwing a new exception with a suitable message tends to need intermediate values from the try-clause which are not available in the catch-clause unless fine-grained try/catch steps are done. Functional programming patterns have reasonable tools to deal with such code. At least “finally” blocks or try-with-resources work well for resource release/cleanup.
So given a lack of a good way to return
(error, output) pairs, it is no surprise that many Java functions use the pattern of returning the computed result only, with any non-success status being reported as an exception instead. I see these kinds of exceptions as an alternative to returning an (error, result), and quite different from exceptions for exceptional circumstances. And as with C or functional languages, such “status return values” should be processed immediately in the calling function. It therefore makes perfect sense for such exceptions to be checked - ie for a method to declare which “failure types” it can generate, and for callers of such methods to be required to correctly deal with them. That makes error-as-exception and error-as-result effectively equivalent.
Or in other words: code calling any function which uses exceptions as a “side-band to return failures which are not truly rare and hard-to-handle” should not let such exceptions propagate far.
Rust’s “?” operator allows a failure to be “passed up to the caller unaltered” where that is appropriate, and redeclaring a checked exception in Java does the same. This doesn’t mean it is always appropriate.
So what should code do when it calls a function that throws a checked exception? In my opinion, it should catch any such exceptions and return a suitable value representing the error - ie convert the exception into a return-value (functional style). If it can’t reasonably do that, then it should throw a new checked exception which represents the failure in terms meaningful to the caller of the function. Where a set of functions are tightly coupled, it might be acceptable to let an exception propagate up a few frames in the call-stack (a “non-local return”) but letting it cross any kind of module/domain boundary is exposing external code to internal details that it cannot, and should not, understand.
When any function allows an exception from a function it calls to propagate up to its caller, this is exposing implementation details and failing to correctly isolate. As a user of a library, if a function takes a
filename parameter then ok, a
FileNotFound error is a reasonable thing for it to return - or for a Java method to throw. However if the function parameters do not explicitly deal in files then such an exception is just unfair, surprising, and dangerous. What the calling function should do is map that error internally into some type that makes sense in terms of the API (parameters) which that function offers to its callers. This applies both to functions that return errors and ones that throw exceptions.
Checked exceptions strongly encourage the implementer of any function to follow the above conventions. Unchecked exceptions do not, tempting developers to leak internal details about a function’s implementation to code higher on the stack. And such leaks are unstable, with the application behaviour potentially changing without warning when a function’s internal implementation changes.
It therefore seems to me that code which extensively uses unchecked exceptions is more error-prone and less maintainable over the long term due to the lack of any kind of encouragement for developers to correctly handle errors.
When “mapping” exceptions, of course preserve the original cause. The stack-trace part of an exception is IMO the primary advantage over “error objects” - although an “error object” can be an exception-object. I’m also not suggesting that every function should have a matching set of exception-types; not only would that require a large number of types but mapping repeatedly would also generate very deep cause-chains. I suggest one or two general exception types per “module/layer” in the code, maybe one for “invalid input” and another for “other errors” - although errors related to things other than “invalid input” sometimes fall into the category of “exceptional problems”, ie can be unchecked exceptions. Of course subtypes of these exception types can be declared as needed, ie if a function thinks it would be useful to report a problem in more detail. But if the type of checked exception reported by a called function is also appropriate for the calling function to throw, then it can just be allowed to propagate through (ie the calling function can declare the same checked exception type).
I believe that libraries should definitely use checked exceptions, in order to clearly document their error-handling behaviour. What errors an API can return (whether via an explicit return-object or via an exception) should be very explicit. An application which uses such libraries can then choose whether to map those exceptions to unchecked exceptions or not. I also believe that layers or modules within a large code-base should follow the same guidelines.
Good test coverage for a code-base supports good error handling regardless of what support the programming language provides (the only way I can imagine a large Python codebase ever working). As has been proven with C#, Kotlin, and other languages that don’t have checked exceptions, it is also possible to create quality software without checked exceptions. However implementing code in a language that supports unchecked exceptions doesn’t mean they should be used everywhere; a function should document its behaviour properly and the kinds of errors it can return is part of that. In particular, just because you’re using Kotlin, which doesn’t support checked exceptions, that doesn’t mean that the correct approach to error-handling is to use unchecked exceptions which propagate through multiple layers of the application.
When working in a language that regularly uses exceptions for non-critical failures (eg Java), correctly handling an error which was triggered multiple levels deeper in a call-stack can be done in two ways:
- via error-handling which doesn’t really care what the cause of the error was, or
- by inspecting the exact cause and taking appropriate action
In the first case, unchecked exceptions might be appropriate. However the error-handling code still needs to know things like:
- whether the error was due to invalid input data (in which case that input should be rejected)
- whether the error was a coding error (in which case the input which triggered that case should probably be skipped at least for now)
- or whether the error was a temporary system problem (in which case retries could be attempted)
Such decisions generally require appropriate context associated with the exception. IMO this means that almost any function should catch exceptions from the functions it calls and throw a different exception which is meaningful with respect to the API that function offers and with appropriate context. This pattern can be seen in the spring-jdbc library for example where SQL exceptions from the underlying database driver are transformed into exceptions that relate to the database operation that the spring-jdbc library was requested to perform.
Code-checking tools such as Sonar even detect functions which do return status values and point out failures to handle those (eg the boolean return-value of
File.delete). It seems somewhat inconsistent to require functions to handle such cases, but allow status returned in the form of an exception to be ignored. Yes, these are not quite the same as exceptions do propagate to the caller - but without appropriate context information.
I could also sum up my opinion as simply being in agreement with Joshua Bloch in his book “Effective Java”:
- Use Exceptions only for exceptional scenarios
- Use checked exceptions for recoverable conditions
- Throw exceptions appropriate to the abstraction
- Don’t ignore exceptions
What is an “exceptional scenario”? That’s a grey area. A SQL exception indicating that the provided SQL command is synactically incorrect is probably an “unexpected condition”; it indicates a programming error that will almost certainly be detected during application testing. It is also a problem that a program cannot “recover from” except at a very high level, eg returning a “500 internal error” status from a REST call. It therefore seems reasonable to treat this as an unchecked exception, in order to avoid cluttering code with error-handling conditions which simply propagate an error upwards. On the other hand, the stack-trace for such an error is important for resolving the problem - and the original exception probably lacks a lot of context that intermediate layers could add. I would therefore suggest that each module/layer/domain of the application should define an unchecked exception type representing “unrecoverable error”, and should map checked or unchecked exceptions representing unrecoverable errors into that type - while adding additional context as needed. Is a SQL exception representing “field too large” also an “unexpected condition”? That isn’t so clear, I think. It does indicate that the program has failed to validate input correctly, but the error is also triggered by specific user input. There is a balance to be found between simplifying code and properly handling errors, and some cases lie near the boundary…
What About Lambdas?
Java supports Lambda Expressions (since v1.8). While the lambda functionality itself works fine with exceptions, unfortunately many of the standard interfaces do not, eg in
someCollection.stream().map(..), the map method is defined to accept an instance of interface
Function with a method
apply which does not throw any exceptions.
I don’t personally see that moving to use unchecked exceptions extensively throughout a code-base is “solving” this problem. If you want to map a collection using some function with a checked exception then the point is that function signature is trying to tell you something; it can fail and you need to handle that case. And not in a “rare and hard-to-handle” way (otherwise it would be using an unchecked exception).
In my opinion, the best solution here is to use the functional programming approach: ensure your mapping function returns something equivalent to
Either<T,V>. Or “tunnel” the exception through the map call, ie use unchecked exceptions only over a clearly bounded set of stack frames; there are several suggestions in this stack-overflow thread.
Yes, it is more verbose. It is also less likely to go boom at runtime than having unchecked (undeclared) exceptions propagating through your callstack.
Arguments In Favour of Unchecked Exceptions
Yes it is true that:
- there are many articles on the internet promoting unchecked exceptions
- and Kotlin uses unchecked exceptions
Before looking at some articles in detail, I think it is important to consider the context for the arguments they put forward. If you are writing a prototype or a small internal tool with low reliability requirements, then yes proper error-handling (including checked exceptions) can get in the way of productivity. But if you are working on a large business-centric application with rapid change, poor documentation, insufficient test coverage, and regularly changing developers (as I regularly do), you need all the compile-time support you can get.
Of course complex apps have also been developed in Kotlin - including IDEA. So yes it clearly is possible to build large software systems without checked exceptions. However has anyone checked how deep through the callstack such exceptions propagate? Maybe they apply some of the functional-error-handling principles above, and strictly limit their use of unchecked exceptions for “non-exceptional” circumstances. In addition, is the code-base and developer-base really comparable to your situation? I’ve got no specific contact with the IDEA team for example, but wouldn’t be surprised if the code development team was small, stable, and highly skilled, and automated code test coverage was high. All those things reduce the need for compile-time (static) help with error-handling.
Here are some specific reasons given in favour of unchecked exceptions:
Adding a new call at a lower level which can fail causes signature-changes in a whole call-stack
Well, if the failure can be dealt with locally, then no there is no additional exception to be propagated.
If it cannot be dealt with locally, but the existing functions already have some way of communicating to their callers that “a failure occurred” which is appropriately dealt with at a higher level, then the new exception can be mapped into the existing one, with appropriate context info, and no method signature changes are required.
If it cannot be dealt with locally, and this new checked exception represents a kind of error that is significantly different from any kind of error the function previously reported to its callers, then there is a real problem that needs to be solved. Callers clearly need to know about this kind of error, and there is no way to communicate that, so the API (return-type or checked-exception-list) does need to change - ideally in a way that allows similar problems to be reported in future without an API change. The existence of checked exceptions just made sure that it does get dealt with rather than being a nice runtime surprise.
Exception types expose implementation details
Well, only if you don’t define proper exception types. Yes, if you expose IOException because that’s what your current implementation throws, and then change the method implementation in a way that can throw other exceptions you have a problem. The cause is not checked exceptions, but instead that IOException was never the right representation for a failure of your method.
Addressing Specific Articles Criticising Checked Exceptions
You Have To Go
In Checked exceptions, I love you but you have to go, the author claims that most catch-blocks “rethrow or print error”. If code is just logging exceptions and ignoring them, then there are definitely problems, so I’m not sure what the point is here. For rethrowing - yes, I would expect that. As I mentioned above, IMO a function should report errors that are understandable in terms of the API that function offers; if it doesn’t take filenames as parameters then it really has no business throwing file-related exceptions. Within a small group of tighly-coupled functions, some slack can be allowed here to save typing. But quality code does require more lines than a “slap it together” solution. Maybe for throwaway tools there is some justification for converting all exceptions to unchecked exceptions - but hey, we can write a helper method for that..
I’m not convinced it is easy to accidentally “eat an exception”; just don’t write empty catch-clauses.
There are a few fair points there: some error-handling is not just “error remapping”, and it can be a little harder to see these cases among simpler error-remapping code. And yes, some catch-clauses can be hard/pointless to reach with unit tests.
I agree that sometimes when prototyping the necessary checked-exception clauses can be a little annoying; it can be nice to get the overall flow done without error-handling first and add that later but Java can get in the way here. However there’s a price to pay for having support to ensure the program is robust in the production phase.
The point about “checked exceptions don’t work well with callback methods such as the visitor pattern” has been covered above with the talk about lambdas. If a function can fail, just hiding that isn’t a good solution. Remapping the exception to a return-value is one solution; “tunnelling” via a runtime exception is also ok, as long as the exception is guaranteed to travel only a small number of stack-frames.
The “loss of fidelity” argument (due to intermediate layers remapping exceptions) doesn’t really persuade me. If you are not bothered by strong coupling between your code’s upper layers and its lower layers then, yes, avoiding such “remapping” can give more control. That’s not a solution that scales well though. Here my background in larger systems might be biasing myu opinion a bit though; when working on codebases built by 20+ developers over 5+ years of coding, such coupling is just not advisable and the slight loss of detailed control is well worth trading for a properly layered architecture.
Why you should ignore exceptions in Java
In this codecamp article, the first point is that “there are many scenarios where the application cannot continue to run and needs to stop”. I’m rather surprised by this argument; I haven’t worked on too many systems where just terminating is a good option. Yes, Erlang follows the “just let it crash” philosophy, but it has surrounding runtime support for process restarts, retries with delays, and various other things. I would also say that in a large application, it is only the top level of code which is able to make such a decision; reusable code in libraries cannot assume that its callers will be working in that mode. But if you are working on an “I don’t care if it crashes on invalid input or unexpected environment” then yes I can see that checked exceptions would be less than optimal. May I suggest Python for such use-cases?
Checked Exceptions are Evil
Philipp Hauer argues that Checked Exceptions are Evil.
He gives an example where simply redeclaring an IOException ends up “polluting” the signature of methods higher in the stack. My answer is: declaring IOException is appropriate in methods
UserRepository.getUsers as their API explicitly deals with type
AddUserAction.execute should be remapping that exception to something relevant to its API:
- when it calls
UserRepository.getUsersin a way that is really not expected to trigger an
IOExceptionthen an unchecked exception may be appropriate
- but when the IOException is something that could reasonably be expected here (eg user-provided data should refer to an existing file) then the method should somehow encode errors into its return-value or should throw a checked exception related to “unable to execute user action”.
With this approach, later changes to AddUserAction which end up calling other methods that throw different types of checked exceptions, there is now a mechanism for reporting “unable to execute user action” so the signature may not need to change again.
The author makes the point that in some circumstances a function may declare a checked exception, but be invoked in a way/environment that can never trigger that exception. In that case, yes I agree that it should be caught and rethrown as an unchecked exception; it becomes an “exceptional situation” in this context instead of a recoverable error”. But only in that context.
The statement that (rephrased) “the client doesn’t care which type of failure occurred” isn’t valid in my opinion. That is the caller’s decision, not the callee’s. Where a set of functions are tightly coupled, it might be acceptable to let caller’s requirements leak into called functions - but care should be taken not to take this too far. It certainly should not happen over any “domain boundary” (module/layer boundary) or the result is hard-to-maintain and unreusable code.
And again (a common point in anti-checked-exception arguments) it is stated that catch-clauses often simply swallow exceptions. I really hope not; in the places I work in there are code-reviews, and such code won’t pass any kind of review.
The remaining points are sufficiently covered elsewhere.
Ignore Checked Exceptions, All the Cool Kids are Doing It
On OverOps, Alex Zhitnitsky looks at some academic analysis of various Java code-bases which shows that most of the catch-clauses were - well, garbage.
Actually, this article isn’t either for or against checked exceptions; it just points that in practice error-handling is very poorly done and having checked exceptions still can’t force developers to do it right. All I can say is: code reviews, people!.
Logging libraries often truncate cause-chains for logged exceptions, losing the actual original point of failure. This is IMO a problem with the logging libraries; the exception-cause-chain is correct when passed to the library, and if the library thinks it best to discard some data from the cause chain, it should not discard the last (deepest, original) cause.
Can we live without checked exceptions? Yes, of course. However I am firmly of the opinion that checked exceptions in library APIs are a massive contribution to robust software. Whether an application (ie code which is not a library) needs checked exceptions depends upon its size and complexity. For larger applications (anything which has “modules”), I believe those internal modules should be treated just like library APIs - ie the errors that the API reports should be explicit, either as return-values or checked exceptions. Anything else will lead to one or more of:
- deep coupling between higher and lower layers of the software stack,
- poor context available in logged exceptions,
- failed opportunities to recover from errors.
It should not be forgotten that whether to do error-handling “properly” or not can be a business decision. My proposal/opinion above produces (in my opinion) robust and maintainable code. However that might not be the most cost-effective solution; sometimes lower quality software may be acceptable in return for lower costs/faster development, even in systems which are not prototypes. In this case, extending the use of unchecked exceptions, and just living with the occasional unexpected failure in production, or unclear codepath, might be an option. It should be a deliberate choice though, not an accidental side-effect of a coding style. At least libraries and other reusable code should be using checked exceptions, so the user of that code can choose whether to implement robust or cheap.
References and Further Reading
- Raymond (Microsoft dev blog): Cleaner, more elegant, and wrong - “Just because you can’t see the error path doesn’t mean it doesn’t exist.”
- VAVR (formerly JavaSlang) - A functional library for Java, including error-handling support