What are exceptions
Exceptions indicate occurences in the programm that are, well exceptional.
Some examples of exceptions are:
- Opening a file that does not exist
- Converting the String "test" to a number.
- Requesting some memory from the OS while it's out of memory.
Some examples that are not exceptional:
- Searching for the first occurence of an element in an array and not finding it.
- Searching a user by name in a database and not finding it.
How languages do exceptions
Programming languages have different ways to indicate that something exceptional occured.
Special return value
These programming languages use a special return value to indicate an exception.
C
test.cint
Here the malloc
function return a value of NULL
to signal an error.
Other calls like puts
(it returns EOF
to indicate an error) use
different values to signal an error.
Generally the return values to indicate exceptions are chosen in a way
that makes it clear that they are not a normal return value. Functions
that return pointers might return NULL
. Functions that return indices
might return -1
.
C programmers can propagate the error upwards the call hierarchy by designing their functions in a way that allows these special values and then returning them:
propagate.c/**
* Returns 0 on success. Other return values indicate errors
* */
int ;
int
int
The myFunc
function handles the error returned by malloc by returning
a special value (1). It would return (0) to indicate succcess.
The programmer might need to convert between different error values in each function.
The tooling does not require the programmer to handle or acknowlege errors but there are extensions that can help. It is for example possible to use attributes like this to enforce the checking of return values:
checked.c/**
* Returns 0 on success. Other return values indicate errors
* */
int ;
int
int
Here the compiler can omit a warning that the return value is ignored:
checked.c: In function 'main':
checked.c:10:2: warning: ignoring return value of 'myFunc', declared
with attribute warn_unused_result [-Wunused-result]
10 | myFunc();
| ^~~~~~~~
Go
Other languages make a clearer distinction between normal return values and error values.
Go for example handles errors like this:
test.gopackage main
import (
"fmt"
"strconv"
)
func main()
Here the function Atoi
returns 2 values. The first being the real value
and the second being the error. If there is no error the second value will
be nil
.
Propagating the error works by passing the error on. As in c the programmer needs to do this manually by return the appropiate value.
propagate.gopackage main
import (
"fmt"
"strconv"
)
func main()
func myFunc() (int, error)
In constrast to c the compiler enforces by default that all return values
must be used. They can also be explicitly ignored by assigning them to _
.
Rust
Rust handles errors by returning a single type that can either contain an error or the result:
test.rsuse File;
In c and go if the error should propagate upwards the pragrammer had to
manually return the error value. In rust the ?
oparator can be used
to achive an early return that propagates the error.
use File;
use Result;
Exceptions in Java
Java does not use a special return value. Instead exceptions are signaled out of band by "throwing" them.
ThrowableExample.java
The method myMethod
has only one return value. But it can still
indicate exceptions. The method has to advertise the exceptions
that can be thrown by adding them to it's signature.
Possible exceptions must be handled by "catching" them. If an exception isn't it will propagate upwards and has therefore to be declared in the method signature of the caller. The compiler enforces this.
Before we talk about the other exception type in java let's pause for a moment and think about what benefits and drawbacks we get from this.
Benefits:
- As with go and rust we know if a method can fail by looking at it's signature.
- As with go and rust (and to some extend c) we can have complex error types that can contain further information about the error.
- As with go and rust the compiler checks proper handling of exceptions.
- Passing exceptions upwards the call stack happens automatically.
Drawbacks:
- Java introduces a special syntax for error handling which is adding complexity to the language. Go and rust avoid this by reusing programming constructs that are used elsewhere in the language.
- It is not clear which line in a piece of code may throw by only looking at how it's used.
For last drawback consider the following code:
WhoThrows.javaExample.method1;
Example.method2;
Example.method3;
From reading this piece of code alone it's impossible to say
which function may throw and abort the normal execution flow. Go
makes this clear by forcing the caller to use the return values. Rust
makes this clear by either handling the return value or by explicity
appending ?
to the function call.
I think this is a medium drawback of java's approach to exceptions. Especially in big methods it's very hard to reason which lines will be executed and which may be skipped.
Unchecked Exceptions
Java handles a lot of programming errors by throwing exceptions. Examples for this are:
- accessing a
null
value. - trying to read an array with an out of bounds index
- dividing an an integer value by zero.
Especially the first two exceptions could appear in almost every line.
Forcing the programmer to declare these Exceptions everywhere would
only clutter the code without making it more clear. It is also
not reasonable to except the programmer to be able to recover from
an exception that occures because of a null
access since this
most likely means that there is a bug in the code. This makes
everything else beyond this point unreliable.
The solution for this in java are the so called unchecked exceptions. The compiler doesn't check if these exceptions are handled or declared in the method signature. Otherwise they behave the same. They still could be handled by being caught and they still propagate upwards if they aren't.
Unchecked exceptions are not a unique privilege of the java creators. Everyone
can create an unchecked exception by letting it extend RuntimeException
.
Which exception type to use and why is it unchecked?
The general guideline is to use checked exception when the caller can be expected to recover from the problem. (See also Unchecked Exceptions — The Controversy by Oracle.)
Springs rest and jdbc abstractions only throw unchecked exceptions. Robert C. Martin writes the following
The debate is over. For years Java programmers have debated over the benefits and liabilities of checked exceptions. When checked exceptions were introduced in the first version of Java, they seemed like a great idea. The signature of every method would list all of the exceptions that it could pass to its caller. Moreover, these exceptions were part of the type of the method. Your code literally wouldn’t compile if the signature didn’t match what your code could do.
At the time, we thought that checked exceptions were a great idea; and yes, they can yield some benefit. However, it is clear now that they aren’t necessary for the production of robust software. C# doesn’t have checked exceptions, and despite valiant attempts, C++ doesn’t either. Neither do Python or Ruby. Yet it is possible to write robust software in all of these languages. Because that is the case, we have to decide—really—whether checked exceptions are worth their price.
— Robert C. Martin "Clean Code: A Handbook of Agile Software Craftmanship" (p. 106)
Why are these projects and people advocating against checked exceptions? Let's take a look at the arguments.
Often they can't be handled
Despite the general guideline, to only use checked exceptions when the caller can recover from them, many argue that this can lead to the following problem:
An often cited exception handling best practice is "throw early catch late". What this means is that an exception should propagate upwards until it arrives at an layer in which it can be reasonably handled.
This best practice applied to projects implementing mostly business logic leads to it just propagating most exceptions upwards. These projects tend to be designed in a similar manner:
+-------------------------+
| Framework |
| +---------------------+ |
| | | |
| | BUSINESS | |
| | LOGIC !!! | |
| | | |
| | (This is the code | |
| | we get payed for) | |
| | | |
| | +-----------------+ | |
| | | Persistence etc.| | |
| | +-----------------+ | |
| +---------------------+ |
+-------------------------+
Only very few exceptions really concern the business layer. Most exceptions are just low-level ones for which we don't care. Examples are
- There is a problem with the DB (no connection, wrong credentials, full disk)
- There is a problem when trying to access a resource via http
- A http request may be illformed
- The client looses it's connection while we write an http response
All these are things that we are not able to handle in our business logic.
As a result our code ends up cluttered with exception propagation in the form
of throws
declarations or wrapping the exceptions in RuntimeException
s.
They don't work when extending JDK (or other 3rd party) classes
We cannot use the the functional interfaces of java. Supplier,Function,Consumer
and so on all don't support checked exceptions. We could write our own variants
that are allowed to throw. But than we wouldn't be able to use those with
standard library calls like 'Arrays.sort', map
or filter
.
The Runnable
interface has the same problem and it's part of Java since
1.0.
The only way to solve this problem is by wrapping the checked exceptions in unchecked ones. Or to not throw checked ones in the first place...
It's also not only functional interfaces that have these problems. Want to
implement your own List
class thats backed by a database or file storage?
Better wrap all your checked exceptions in unchecked ones to allow the
code to compile.