Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Four Problems With Java's Exceptions and How Scala Can Help

DZone's Guide to

Four Problems With Java's Exceptions and How Scala Can Help

Here are four common problems with Java exceptions and how Scala can help, including control flow and the Try type.

· Java Zone ·
Free Resource

Java-based (JDBC) data connectivity to SaaS, NoSQL, and Big Data. Download Now.

Error handling is important for many common operations — from dealing with user input to making network requests. An application shouldn’t crash just because a user enters invalid data or a web service returns a 500. Users expect software to gracefully handle errors, either in the background or with a user-friendly and actionable description of the issue. Unfortunately, since dealing with exceptions can be messy and complicated, error handling often comes into play as an almost-forgotten last step for polishing an application.

We’ll cover four ways to deal with Java’s exceptions and then wrap up with a few ways modern languages like Scala can help programmers make sure they correctly handle errors.

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 while attempting to prepare an application for production.

Java introduced checked exceptions in an attempt to solve this problem by requiring users to either annotate the function that might throw that exception or by catching the exception immediately. While checked exceptions are somewhat guarded by the compiler, it’s still too easy to add a throws 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.

A flowchart demonstrating a confusing exception control flow

Exception Control Flow

The blue arrow highlights any remaining statements that 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.

As you can see, it can be difficult to remember if an exception has been handled previously without drawing a diagram (such as the one above) to keep track of everything. As a result, the top-level function often ends up acting as a catch-all, making the errors much less meaningful, because they are handled far from their source. In a complex codebase, it’s also easy to forget that a function might throw an exception, causing you to erroneously reuse it without wrapping the call in a 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 properly.

4. Exceptions Are Runtime, Not Compile-Time, Errors

Exceptions can be used to handle unusual states; therefore, 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 the 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 an Option instead of null can help to avoid NullPointerExceptions.

A few of Scala’s Solutions to the Problems With Exceptions

Of course, you can use the classic try/catch when handling an exception directly, however, using the following types in Scala makes it easier to return the possibility of an error state via the type system.

Try

When a call to the external library or API can throw an exception, you are able wrap the result in the Try 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 Tryobject 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.


Try demo 1:
// Parses a String into an Int and then adds 1. 
// If parsing fails, the error is passed up the chain.
def plus1(s: String): Try[Int] = {
  parseInt(s).map(_ + 1)
}


Try demo 2:

// Parses a String into an Int. 
// If parsing fails, the error is handled by returning the length of the String.
def parseIntOrGetStringLength(s: String): Int = {
  parseInt(s) match {
    case Success(i) => i
    case Failure(e) =>
      // Easy to add logging here
      s.length
  }
}


Either

When handling your own error cases, you can use an Either type to return 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."))
  }
}


Similar to Try, the Either type forces the caller to handle the error case in some manner. By using the 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.

Either demo 1:
// Gets the length of a substring. 
// If getting the substring fails, the error is passed up the chain.
def stringLengthAfterSubstring(s: String, start: Int): Either[Error, Int] = {
    substring(s, start).map(_.length)
}



Either demo 2:
// Gets the length of a substring. 
// If getting the substring fails, the error is handled by returning a default of 0.
def stringLengthAfterSubstringWithDefault(s: String, start: Int): Int = {
    substring(s, start) match {
        case Right(result) => result.length
        case Left(error) =>
            // Log error
            0
    }
}

A more in-depth guide to 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 reduce 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.

Connect any Java based application to your SaaS data.  Over 100+ Java-based data source connectors.

Topics:
java ,exception ,either ,throws ,scala

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}