56

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

 5 years ago
source link: https://www.tuicool.com/articles/hit/qayA7rv
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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.

QN32MbJ.png!web

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 NullPointerException s.

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 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.

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:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK