When Java came out some decades ago, it was pretty innovative at the time. In particular, its exception handling mechanism was a great improvement over previous C/C++. For example, in order to read from the file, there could be a lot of exceptions happening: the file can be absent, it can be read-only, etc.<!--more-→
The associated Java-like pseudo-code would be akin to:
File file = new File("/path");
if (!file.exists) {
System.out.println("File doesn't exist");
} else if (!file.canRead()) {
System.out.println("File cannot be read");
} else {
// Finally read the file
// Depending on the language
// This could span seveal lines
}
The idea behind separated try
catch
blocks was to separate between business code and exception-handling code.
try {
File file = new File("/path");
// Finally read the file
// Depending on the language
// This could span seveal lines
} catch (FileNotFoundException e) {
System.out.println("File doesn't exist");
} catch (FileNotReadableException e) {
System.out.println("File cannot be read");
}
Of course, the above code is useless standalone. It probably makes up the body of a dedicated method for reading files.
public String readFile(String path) {
if (!file.exists) {
return null;
} else if (!file.canRead()) {
return null;
} else {
// Finally read the file
// Depending on the language
// This could span seveal lines
return content;
}
}
One of the problem with the above catch
blocks is that they return null
. Hence:
- Calling code needs to check everytime for
null
values - There’s no way to know whether the file was not found, or if it was not readable.
Using a more functional approach fixes the first issue and hence allows methods to be composed.:
public Optional<String> readFile(String path) {
if (!file.exists) {
return Optional.empty();
} else if (!file.canRead()) {
return Optional.empty();
} else {
// Finally read the file
// Depending on the language
// This could span seveal lines
return Optional.of(content);
}
}
Sadly, it changes nothing about the second problem. Followers of a purely functional approach would probably discard the previous snippet in favor of something like that:
public Either<String, Failure> readFile(String path) {
if (!file.exists) {
return Either.right(new FileNotFoundFailure(path));
} else if (!file.canRead()) {
return Either.right(new FileNotReadableFailure(path));
} else {
// Finally read the file
// Depending on the language
// This could span seveal lines
return Either.left(content);
}
}
And presto, there’s a nice improvement over the previous code. It’s now more meaningful, as it tells exactly why it failed (if it does), thanks to the right part of the return value.
Unfortunately, one issue remains, and not a small one. What about the calling code? It would need to handle the failure. Or more probably, let the calling code handle it, and so on, and so forth, up to the topmost code. For me, that makes it impossible to consider the exception-free functional approach an improvement.
For example, this is what happens in Go:
items, err := todo.ReadItems(file)
if err != nil {
fmt.Errorf("%v", err)
}
This is pretty fine if the code ends here.
But otherwise, err
has to be passed to the calling code, and all the way up, as described above.
Of course, there’s the panic
keyword, but it seems it’s not the preferred way to handle exceptions.
The strangest part is this is exactly what people complain about with Java’s checked exceptions: it’s necessary to handle them at the exact location where they appear and the method signature has to be changed accordingly.
That's what Java devs do in checked exceptions catch blocks ... and why I believe they are just useless verbosity pic.twitter.com/52bYuaZRWL
— Mario Fusco (@mariofusco) June 6, 2016
For this reason, I’m all in favor of unchecked exceptions. The only downside of those is that they break purely functional programming - throwing exceptions is considered a side-effect. Unless you’re working with a purely functional approach, there is no incentive to avoid unchecked exceptions.
Moreover, languages and frameworks may provide hooks to handle exceptions at the topmost level. For example, on the JVM, they include:
- In the JDK,
Thread.setDefaultUncaughtExceptionHandler()
- In Vaadin,
VaadinSession.setErrorHandler()
- In Spring MVC,
@ExceptionHandler
- etc.
This way, you can let your exceptions bubble up to the place they can be handled in the way they should. Embrace (unchecked) exceptions!