4 Problems with Java's Exceptions and How Scala Can Help
Trudy Firestone
Reading time: about 8 min
Topics:
1. Exceptions are easy to miss
Java's exceptions allow the caller of a function to ignore any errors the function might produce. If the program completely fails to catch an exception, the program will crash. While ignoring error cases can be useful for putting together a quick prototype, it can be difficult to track down all the places where an exception can be thrown when you attempt to ready an application for production. Java introduced checked exceptions in an attempt to solve this problem by enforcing either annotating the function that might throw that exception or catching the exception immediately. While checked exceptions are somewhat guarded by the compiler, it’s still too easy to add athrows
clause or wrap the exception in a try/catch
block without paying any attention to the error case and neglecting to handle the exception properly. In addition, only a small portion of Java’s exceptions are checked, so many exceptions are still very easy to miss.
2. Exception control flow is hard to follow
If exceptions are a common or even essential part of your application, it becomes increasingly difficult to understand the codebase as it grows larger and more complex. Rather than following the usual flow of data through parameters and return values, Java's exceptions occur outside the normal function pattern, resulting in confusing and fragile code. As an example, consider the diagram below.The blue arrow highlights that any remaining statements will be skipped once an exception is thrown. The green arrow shows how an exception can jump several levels up the stack before being handled in a catch
.
try/catch
. This results in a crash, as demonstrated in Component 2 of the diagram above.
With all of these factors, removing the error from the normal path of data by wrapping it in an exception adds an unnecessary level of complexity, making the overall result of a code path harder to predict.
3. Normal events are treated as exceptional
Quite often, Java's exceptions are used in ways that make normal behavior seem unexpected. For example, if a user is supposed to type in a date, but they type“hello”
instead, code that’s meant to parse the date might throw an exception instead of returning a Date
object. Suddenly, the perfectly ordinary occurrence of a user not following guidelines becomes an exceptional case and the function caller is responsible for remembering to handle the exception.
As another example, suppose the program throws an exception when a network request returns a 404 Not Found error. While the client may initially expect an endpoint to continue to exist, it’s not unreasonable for the endpoint to be removed. Although throwing an exception on 404 responses may initially seem like a good idea, it treats a common occurrence as an exceptional one, making it easy to forget to handle it properly.
4. Exceptions are runtime, not compile-time, errors
Exceptions can be used to handle unusual states, so it’s easy to miss them when testing. Although we generally test major user flows and any edge cases we can think of, error cases are often left out because they can be difficult to reproduce. Exceptions may also hide in edge cases that you forget to test. Because Java's exceptions are checked at runtime, simply compiling the code is not enough to make sure the error cases are properly handled. The error has to actually be triggered. However, it’s often possible to use better types which can move error checking from runtime check to a compile-time one. For instance, using anOption
instead of null
can help to avoid NullPointerException
s.
A few of Scala’s solutions to the problems with exceptions
Of course, you can use the classictry/catch
when handling an exception directly, but using the following types in Scala makes it easier to clearly return the possibility of an error state via the type system.
Try
When a call to an external library or API can throw an exception, you can wrap the result in theTry
type and return that value.
def parseInt(s: String): Try[Int] = {
Try(Integer.parseInt(s))
}
This forces the caller of the function to recognize that parseInt
can have an error state. The Try
object will either contain the parsed Int
or the exception object (in this case, a NumberFormatException
). In order to access the desired value, the caller will have to handle the error, either by mapping over the Try
and passing the error up the chain or by directly handling both the Success
and Failure
cases.
Either
When handling your own error cases, you can use anEither
to return either the value or some sort of error object. Scala’s Either
has a Left
and a Right
value. By convention, the Left
value is the error object.
def substring(s: String, start: Int): Either[Error, String] = {
if (start <= s.length) {
Right(s.substring(start))
} else {
Left(Error("Start index out of bounds. String was too short to get substring."))
}
}
Similarly to Try
, the Either
type forces the caller to handle the error case in some manner. By using Scala 2.12’s right biased Either or the cats library in earlier versions of Scala, you can map over the Right
of an Either
like you can map over a Try
. Alternatively, you can handle the Right
and Left
cases directly.
Try
and Either
can be found in The Neophyte’s Guide to Scala.
Exceptions need to be handled with care. When misused, they can make your program unnecessarily complex. In the worst case, they can cause your program to crash unexpectedly. Luckily, Scala provides a few types that make it easy to move the risk of exceptions into the safety of the type system. By using the Try
and Either
types and handling unavoidable exceptions as close to the source as possible, it’s a lot easier to make a robust, user-friendly application.About Lucid
Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.