Java - Exceptions from beginner to Pro

Cristian Toader
11 min readDec 29, 2020

--

In this article I’ve decided to go back to the Java language fundamentals and talk about exceptions. They are a core part of the Java language, but is there more to them than the fundamental catch-throw basics? We’ll have a look at exceptions from different perspectives and levels of working with Java applications, and what options we have in handling them.

So why beginner to pro? Well, this article is a journey through a collection of personal thoughts and findings about exceptions. I am going to present them as I’ve learned about them in time, and how I’ve discovered their usages up until the current days.

We will go through exceptions from basic handling and concepts, usages in web applications, compatibility problems and solutions with the stream API, as well as a functional approach to exceptions.

There is no wrong vs right scenario in using exceptions necessarily, but I’d like us to understand a few use cases and what options and freedoms you have when writing Java applications.

Basic exception handling

Let’s start from the beginning, you’re just getting started with Java, playing a bit with the main method and following some inheritance tutorials or university assignment. You’re feeling adventurous and want to try something a bit more complex, so let’s say you decide to read the input of your new application from a file.

But what do we have here?

Probably the first time you’ve encountered exceptions was in the context of simply trying to read or write a file to disk.

What is most tempting to do at this point is to just apply the all so common pattern of IDE Driven Development (IDD) and let the IDE write code for you by either declaring it in the signature or adding a try-catch block.

But let’s stop and ask ourselves.. what is an exception anyway? Well according to the the Oracle documentation an exception is first of all an exceptional event. After being thrown an exception is propagated through the call stack until it either reaches a handler (catch block) or terminates the runtime system.

To go just a bit more through the theory basics, there are two types of exceptions: checked exceptions, and unchecked (runtime) exceptions.

Checked exceptions extend the Exception class and they are the ones that your compiler will complain about just as in the screenshot before. There is a chain of compiler complaints or constraints so to speak, which:

  • Starts with the instruction of throwing a checked exception (e.g. throw new IOException())
  • Continues by declaring in your method signature that you can throw it (e.g. throws IOException)
  • Ends with it being surrounded by a try-catch block (e.g. catch(IOException e) {…}) .

The purpose of checked exceptions is that API library developers can use them to tell you that (un)predictable behavior might happen when calling their code. Based on this, the compiler forces you to be prepared, and offers you the freedom of taking different types of action based on the exception type.

Truth be told, in practice 90% of checked exception cases you won’t be bothered with the specifics of the exception that can be thrown by a method and will catch the parent Exception class, log the exception, and shut down gracefully. The other 10% of times you’ll maybe have some sort of retry logic, or reporting mechanism, or a fallback way of making the process resilient without the need of human interaction.

As for the other type of exceptions, unchecked exceptions extend the RuntimeException class and are supposed to be programming errors that were not foreseen by developers, such as: a null pointer exception (classic), arithmetic exceptions (e.g. divide by 0), array index out of bounds, and others.

It is not uncommon to manually throw runtime exceptions as well, but in this case the compiler will not force you to declare or handle the exception as in does with checked exceptions.

The only pitfall is misusing exceptions by relying on them too much for programming logic which would make the application both very hard to follow (similar to goto clauses in older C applications) and also slower as the exception handling mechanism is heavier than the normal program execution one. As long as you remember that exceptions should be exceptional, everything should be fine.

Exceptions and HTTP responses in web applications

In general checked exceptions eventually lead to try-catch blocks, which in my opinion make code less readable by nesting it in blocks.

But is there a nicer use case for exceptions which makes your code more readable and easier to understand instead?

Yes there is! Spring is one of the most popular Java frameworks used in the enterprise world, and Spring boot web found a very elegant and useful use case for exceptions within web applications.

If you remember, if left unhandled exceptions will climb up the call stack trace and eventually terminate your thread. Spring took this idea, and applied it at an HTTP request level. Obviously getting a backend exception due to an HTTP request won’t terminate the server. Instead, by default it will be handled primitively and result in a 500 HTTP code (internal sever error) as well as a stack trace of what went wrong; but this is rarely what you want!

By leveraging the Proxy design pattern and the Spring AOP (aspect oriented programming) concept, the developers decided to introduce the functionality called controller advice. What it does essentially is map different types of exceptions to different HTTP status error codes and messages. This way you write less code.

For example when a resource is not found, you can simply throw a custom exception which is representative for the HTTP 404 code as well as a specific message.

How this feature probably works in Spring, or at least how I’ve implemented it in a web API framework which didn’t have spring controller advices was by using aspects to add pointcuts over API methods, intercept the calls with a before and after advice, and try-catch the join-point to map any exceptions to the required response.

Controller advices offer a powerful way of writing readable and easy to maintain code. Instead of having to construct custom ResponseEntity logic, you keep both your controller and service layers light, and focus purely on the domain logic while the controller advices enable the application logic responsible of mapping different error status codes based on your desired API.

Exceptions and the Stream API

One of the annoying things in Java 8 streams is that Java is not designed as a functional programming language, but the stream API was designed for functional programming and favors pure functions. However, functions and methods which throw Exceptions are not pure functions because the possibility of throwing an exception means they don’t offer referential transparency, and throwing an exception is not a return value.

So in practice, the problem is that you end up with this type of compilation errors.

So let’s fall back again on “IDE driven development” then, right?

Ugly.. There’s a reason lambdas passed to stream API methods should only have 1 line, and I hope it’s quite obvious to everyone why from the sample code above.

But hey, there’s always the other standard option, let’s throw it in the method signature.

Wait, what? But I’ve went through both options, what should I do now? Why is it not working, I am declaring the exception right there! If you look at the error message it says that you have an unhandled IOException but that is not the real reason.

The reason the code above is not compiling has to do with the method signature of the Function functional interface. If we take a look at what parameter the map function on the stream API takes, it is a Function<? super T, ? extends R> and Function has the following interface signature.

However if you would think about what parameter you are trying to pass in to the stream map method is in fact more along these lines.

You cannot even say that an ExceptionalFunction could ever even extend the Function interface present in the JDK because overriding rules would be broken in the child interface due to the Exception declaration in the signature. So you couldn’t ever pass an ExceptionalFunction to a map method.

Anyway, coming back to the problem, a quick yet not maintainable way of overcoming this practical issue of functional programming not favoring methods which throw exceptions is to extract the try-catch example above in a method such that the code becomes readable. I probably should not give you ideas such as this one. The problem with the approach is that you won’t encounter this type of problem only once in your whole project. It’s a fairly common occurrence, so you’ll end up creating many such methods as the one below (don’t do that — nothing safe about it!).

The smarter thing to do is to use generics, and rather than replicate the method in the example above in 50 different places and ways, just write it once in a generic and reusable way both for yourself as well as for your colleagues. The approach here is to convert something that you don’t want (i.e. ExceptionalFunction) to a Function as shown below, and all of a sudden there is no more unhandled exception cerror.

This is quite a nice example of how to use generics and functional interfaces in order to write neat reusable code.

FYI functions such as the one above that take functions as parameters or return functions are called higher order functions.

But don’t you think that dealing with methods that throw exceptions in the stream API is a very common problem? How likely is it that you’re the first to have solved it?

Aaaand of course it’s been solved already! If you don’t mind adding an extra dependency to your pom dependencies file you can find this feature in the Jool library using Unchecked.function, as well as using the Lombok @SneakyThrows annotation.

A functional approach to exceptions

Back to functional programming principles however, it would be nice to have a cleaner way to handle these exceptions. In the end, the solutions I’ve highlighted in the previous section either suppress the exception and log it, or can re-throw the exception as a runtime exception. These are more of a quick hack to get by and get rid of the exception.

Developers which have worked with more functional programming languages such as Scala, know that there is a better way to handle exceptions, which is by using Try objects.

In simple terms a Try object in Scala wraps a computation that might result in an exception. It is like an Optional object in Java only that instead of Some or None it is either a Success or a Failure. Just as Optional, it supports common methods also found on the stream API such as map, flatmap, filter, get and others.

In more formal terms a Try object is a monadic container type. It encapsulates a higher order function turning it into a monadic value, and offers a series of functions called monadic functions which output other monadic values. This allows for the declarative pipeline/chain type API that we love to see.

Ok but how does that help us, so what.. you want to tell me Scala is better than Java?

Well, that wasn’t the point that I was trying to make. Thanks to the developers of Vavr, a Java framework which I am a big fan of, we have this functionality as well as many others out of the box! Vavr supports try objects in Java and therefore a cleaner way of handling exceptions.

Remember that old classic interview question “does the finally block still execute if you return inside the try-catch block”? Well who cares, there is a much better way to handle exceptions without having to know their gotchas and inner imperative workings and that is by using try objects.

I’ve tried to build a more complex example in order to highlight the feature, I hope it makes my point. Regardless, I didn’t yet come across a situation where you cannot use Try objects and have to rely on traditional/manual try-catch blocks.

Scenarios can be much simpler, without any recover or match patterns. You can simply wrap a call using a Try, and then process it further using map, filter, get, getOrElse, etc.

Let’s imagine a use case where you don’t want to treat the Try or exception in the same method where it occurred, just as you would traditionally by adding throws to the method signature and the try-catch block a few call stacks upwards.

But wouldn’t it be much more intuitive and flexible, if your method would return a Try instead of declare a throws? That way you know that the method may have not executed successfully, regardless it would return an attempted result and in case of a Failure you can choose to pass that to an exception handler component using composition (just playing with thoughts here), rather than being forced to handle it only somewhere on the call stack trace.

To close the topic, my advice is to think a bit about this functionality if you are new to it, familiarize yourself with the concept and think how this could be useful in your projects with an open mind.

Conclusion

To wrap up this article both Java and the frameworks around it have come a long way, and Exceptions have evolved as well. This article is meant as a teaser to encourage you to explore more on this topic. For example we didn’t talk at all about the options you have for handling exceptions using the reactor core Flux API, and that’s also interesting and rich.

Of course in our day to day jobs we still need to interact either with legacy code, or libraries which declare checked exceptions, but that doesn’t mean we need to write code like we did 10 years ago. Today we have options (and trys)!

I hope you’ve found this article useful, and please feel free to share a comment below!

--

--

Cristian Toader
Cristian Toader

Written by Cristian Toader

I am an Engineering Manager at LSEG. I am passionate about technology as well as leadership, psychology, and coaching.

No responses yet