Skip to main content

One post tagged with "java"

View All Tags

· 9 min read
Nikita Lazarev-Zubov

Exception Handling

The first version of Java was released in 1995 based on the great idea of WORA (“write once, run anywhere”) and a syntax similar to C++ but simpler and human-friendly. One notable language invention was checked exceptions—a model that later was often criticized.

Let’s see if checked exceptions are really that harmful and look at what’s being used instead in contemporary programming languages, such as Kotlin and Swift.

Good Ol’ Java Way

Java has two types of exceptions, checked and unchecked. The latter are runtime failures, errors that the program is not supposed to recover from. One of the most notable examples is the notorious NullPointerException.

The fact that the exception is unchecked doesn’t mean you can’t handle it:

Object object = null;
try {
System.out.println(object.hashCode());
} catch (NullPointerException npe) {
System.out.println("Caught!");
}

The difference between a checked and unchecked exception is that if the former is raised, it must be included in the method’s declaration:

void throwCustomException() throws CustomException {
throw new CustomException();
}

static class CustomException extends Exception { }

The compiler will make sure that it’s handled— sooner or later. The developer must wrap the throwCustomException() with a try-catch block:

try {
throwCustomException();
} catch (CustomException e) {
System.out.println(e.getMessage());
}

Or pass it further:

void rethrowCustomException() throws CustomException {
throwCustomException();
}

What’s Wrong with the Model

Checked exceptions are criticized for forcing people to explicitly deal with every declared exception, even if it’s known to be impossible. This results in a large amount of boilerplate try-catch blocks, the only purpose of which is to silence the compiler.

Programmers tend to work around checked exceptions by either declaring the method with the most general exception:

void throwCustomException() throws Exception {
if (Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 2 == 0) {
throw new EvenDayException();
} else {
throw new OddDayException();
}
}

Or handling it using a single catch-clause (also known as Pokémon exception handling):

void throwCustomException()
throws EvenDayException, OddDayException {
// ...
}

try {
throwCustomException();
} catch (Exception e) {
System.out.println(e.getMessage());
}

Both ways lead to a potentially dangerous situation, when all possible exceptions are sifted together, including everything that is not supposed to be dismissed. Error-handling blocks of code also become meaningless, fictitious, if not empty.

Even if all exceptions are meticulously dealt with, public methods swarm with various throws declarations. This means all abstraction levels are aware of all exceptions that are thrown around it, compromising the principle of information hiding.

In some parts of the system, where multiple throwing APIs meet, a problem with scalability might emerge. You call one API that raises one exception, then call another that raises two more, and so on, until the method must deal with more exceptions than it reasonably can. Consider a method that must deal with these two:

void throwsDaysExceptions() throws EvenDayException, OddDayException  {
// …
}
void throwsYearsExceptions() throws LeapYearException {
// …
}

It's doomed to have more exception-handling code than business logic:

void handleDate() {
try {
throwsDaysExceptions();
} catch (EvenDayException e) {
// ...
} catch (OddDayException e) {
// ...
}
try {
throwsYearsExceptions();
} catch (LeapYearException e) {
// ...
}
}

And finally, the checked exception approach is claimed to have a problem with versioning. Namely, adding a new exception to the throws section of a method declaration breaks client code. Consider the throwing method from the example above. If you add another exception to its throws declaration, the client code will stop compiling:

void throwException()
throws EvenDayException, OddDayException, LeapYearException {
// ...
}

try {
// Unhandled exception: LeapYearException
} catch (EvenDayException e) {
// ...
} catch (OddDayException e) {
// ...
}

The Kotlin Way

Sixteen years after Java was first released, in 2011, Kotlin was born from the efforts of JetBrains, a Czech company founded by three Russian software engineers. The new programming language aimed to become a modern alternative to Java, mitigating all its known flaws.

I don’t know any programming language that followed Java in implementing checked exceptions, Kotlin included, despite the fact it targeted JVMs. In Kotlin, you can throw and catch exceptions similarly to Java, but you’re not required to indicate an exception in a method’s declaration. (In fact, you can’t):

class CustomException: Exception()

fun throwCustomException() {
throw CustomException()
}

fun rethrowCustomException() {
try {
throwCustomException()
} catch (e: CustomException) {
println(e.message)
}
}

Even catching is up to the programmer:

fun rethrowCustomException() {
throwCustomException() // No compilation errors.
}

For interoperability with Java (and some other programming languages), Kotlin introduced the @Throws annotation. Although it’s optional and purely informative, it’s required for calling a throwing Kotlin method in Java:

@Throws(CustomException::class)
fun throwCustomException() {
throw CustomException()
}

From One Extreme to Another

It may seem that programmers can finally breathe easy, but, personally, I think by solving the original problem, this new approach—Kotlin’s exceptions model—creates another. Unscrupulous developers are free to entirely ignore all possible exceptions. Nothing stops them from quickly wrapping a handful of exceptions with a try-catch expression and shipping the result to their end users, with a prayer. Or not covered exceptions are going to be discovered by end users.

Even if you’re a disciplined engineer, you’re not safe: Neither the compiler nor API will alert you to exceptions lurking inside. There’s no reliable way to make sure that all possible errors are being properly handled.

You can only guard yourself from your own code, patiently annotating your methods with @Throws. Though, even in this case, the compiler will tell you nothing and it’s easy to forget one exception or another.

The Swift Way

Swift first appeared publicly a little later, in 2014. And again, we saw something new. The error-handling model itself lies somewhere between Java’s and Kotlin’s, but how it works together with the language’s optionals is incredible. But first things first.

Of course, Swift has runtime, “unchecked”, errors—an array index out of range, a force-unwrapped optional value turned out to be nil, etc. But unlike Java or Kotlin, you can’t catch them in Swift. This makes sense since runtime exceptions can only happen because of a programming mistake, or intentionally (for instance, by calling fatalError()).

The rest of exceptions are errors that are explicitly thrown in code. All methods that throw anything must be marked with the throws keyword, and all code that calls such methods must either handle errors or propagate them further. Looks familiar, doesn’t it? But there’s a catch.

Fly in the Ointment

Let’s look at an example from above:

func throwError() throws {
if (Calendar.current.component(.day, from: Date()) % 2 == 0) {
throw EvenDayError()
} else {
throw OddDayError()
}
}

As you can see, you don’t declare specific errors that a method can throw; you’re only required to mark it as throwing something. The consequence of this is that you, again, don’t really know what to catch.

Unfortunately, the code below won’t compile:

do {
/*
Errors thrown from here are not handled because the enclosing
catch is not exhaustive
*/
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is OddDayError {
print(String(describing: EvenDayError.self))
}

You always have to add Pokémon handling:

do {
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is OddDayError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}

In fact, the Swift compiler doesn’t care about specific error types that you try to catch. You can even add a handler for something entirely irrelevant:

do {
try throwError()
} catch is EvenDayError {
print(String(describing: EvenDayError.self))
} catch is IrrelevantError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}

Or you can have only one default catch block that covers everything:

    do {
try throwError()
} catch {
print(error)
}

Another bad thing about the approach is that, without a workaround, you can’t catch one error and propagate another. The only way to implement such behavior is to catch the error you’re interested in and throw it again:

func rethrow() throws {
do {
try throwError()
} catch is EvenDayError {
throw EvenDayError() // Here's the trick.
} catch is IrrelevantError {
print(String(describing: EvenDayError.self))
} catch {
print(error)
}
}

Ointment

In my opinion, Swift’s strongest merit is its optionals system that cooperates with all aspects of the language. If you don’t care about thrown errors, instead of fictitious catch-blocks, you can always write try? Execution of the method will stop the moment the error is thrown, without propagating it further:

try? throwError()

If you’re feeling bold, you can use try! instead of try?, which won’t suppress the error if it’s thrown, but will let you omit the do-catch block:

try! throwError()

This method also allows converting a throwing call to a value. try? will give you an optional one, whereas try! has an effect similar to force-unwrapping:

func intOrError() throws -> Int {
// …
}

let optionalInt = try? intOrError() // Optional(Int)
let dangerousCall = try! intOrError() // Int or die!

Conclusion

Personally, I find Kotlin’s way, ahem, a failure. I can understand why Kotlin developers decided not to follow Java in its way of checked exceptions, but ignoring exceptions entirely, without a hint of static checks, is too much.

On the other hand, is the Java way really that harmful? No mechanism can defend software from undisciplined programmers. Even the best idea can be distorted and misused. But applying Java’s principles as designed can lead to good results.

Connecting two levels of abstraction, you can catch errors from one level and re-throw new types of errors to propagate them to the next level. You can catch several types of errors, “combine” them into one another, and throw them for further handling. This can help mitigate problems with encapsulation and scalability. For instance:

void throwCustomException() throws CustomException {
try {
throwDayException();
} catch (EvenDayException | OddDayException e) {
throw new CustomException();
}
}

What Java lacked from the very beginning is Swift’s optionality system and a syntax to bind exception handling and optional values. I believe, coupled with entirely static checks of thrown exceptions, this would build a very strong model that can satisfy the grouchiest programmers. Although, in any aforementioned programming language, this would require breaking changes, I personally believe it would be a game-changing improvement of code safety.

And if you want to improve your app stability right now, Shipbook is already here for you! It proactively inspects your app, catches exceptions and allows you to analyze them even before your users stumble upon the problem.