What are exceptions

Exceptions indicate occurences in the programm that are, well exceptional.

Some examples of exceptions are:

  • Opening a file that does not exist
  • Converting the String "test" to a number.
  • Requesting some memory from the OS while it's out of memory.

Some examples that are not exceptional:

  • Searching for the first occurence of an element in an array and not finding it.
  • Searching a user by name in a database and not finding it.

How languages do exceptions

Programming languages have different ways to indicate that something exceptional occured.

Special return value

These programming languages use a special return value to indicate an exception.

C

test.c
#include<stdlib.h>
#include<stdio.h>

int main(void) {
	char *mem = malloc(1);
	if (mem == NULL) {
		if (puts("Error: Could not allocate memory!") == EOF) {
			// could not write
		}
		return EXIT_FAILURE;
	}
	return EXIT_SUCCESS;
}

Here the malloc function return a value of NULL to signal an error. Other calls like puts (it returns EOF to indicate an error) use different values to signal an error.

Generally the return values to indicate exceptions are chosen in a way that makes it clear that they are not a normal return value. Functions that return pointers might return NULL. Functions that return indices might return -1.

C programmers can propagate the error upwards the call hierarchy by designing their functions in a way that allows these special values and then returning them:

propagate.c
#include<stdlib.h>
#include<stdio.h>

/** 
 * Returns 0 on success. Other return values indicate errors
 * */
int myFunc(void);

int main(void) {
	if (myFunc()) {
		return EXIT_SUCCESS;
	} else {
		puts("Error while calling myFunc");
		return EXIT_FAILURE;
	}
}

int myFunc(void) {
	char *mem = malloc(1);
	if (mem == NULL) { 
		return 1;
	}
	return 0;
}

The myFunc function handles the error returned by malloc by returning a special value (1). It would return (0) to indicate succcess.

The programmer might need to convert between different error values in each function.

The tooling does not require the programmer to handle or acknowlege errors but there are extensions that can help. It is for example possible to use attributes like this to enforce the checking of return values:

checked.c
#include<stdlib.h>
#include<stdio.h>

/** 
 * Returns 0 on success. Other return values indicate errors
 * */
__attribute__ ((warn_unused_result)) int myFunc(void);

int main(void) {
	myFunc();
}


int myFunc(void) {
	char *mem = malloc(1);
	if (mem == NULL) { 
		return 1;
	}
	return 0;
}

Here the compiler can omit a warning that the return value is ignored:

checked.c: In function 'main':
checked.c:10:2: warning: ignoring return value of 'myFunc', declared
                with attribute warn_unused_result [-Wunused-result]
   10 |  myFunc();
      |  ^~~~~~~~

Go

Other languages make a clearer distinction between normal return values and error values.

Go for example handles errors like this:

test.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	if parsed, err := strconv.Atoi("someVal"); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(parsed)
	}
}

Here the function Atoi returns 2 values. The first being the real value and the second being the error. If there is no error the second value will be nil.

Propagating the error works by passing the error on. As in c the programmer needs to do this manually by return the appropiate value.

propagate.go
package main

import (
	"fmt"
	"strconv"
)

func main() {
	if parsed, err := myFunc(); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(parsed)
	}
}

func myFunc() (int, error) {
	if parsed, err := strconv.Atoi("someVal"); err != nil {
		return 0, err
	} else {
		return parsed, nil
	}
}

In constrast to c the compiler enforces by default that all return values must be used. They can also be explicitly ignored by assigning them to _.

Rust

Rust handles errors by returning a single type that can either contain an error or the result:

test.rs
use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}

In c and go if the error should propagate upwards the pragrammer had to manually return the error value. In rust the ? oparator can be used to achive an early return that propagates the error.

propagate.rs
use std::fs::File;
use std::io::Result;

fn main() {
    let _ = match my_func() {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}

fn my_func() -> Result<File> {
    let file = File::open("hello.txt")?;
    print!("Successfully opened file {:?}", file);
    Ok(file)
}

Exceptions in Java

Java does not use a special return value. Instead exceptions are signaled out of band by "throwing" them.

ThrowableExample.java
public class ThrowableExample {
	public static void main(String[] args) throws MyThrowable {
		System.out.println(myFunc());
	}

	static int myFunc() throws MyThrowable {
		if (Math.random() < 0.5) {
			throw new MyThrowable();
		} else {
			return 42;
		}
	}

	static class MyThrowable extends Throwable {
	}
}

The method myMethod has only one return value. But it can still indicate exceptions. The method has to advertise the exceptions that can be thrown by adding them to it's signature.

Possible exceptions must be handled by "catching" them. If an exception isn't it will propagate upwards and has therefore to be declared in the method signature of the caller. The compiler enforces this.

Before we talk about the other exception type in java let's pause for a moment and think about what benefits and drawbacks we get from this.

Benefits:

  • As with go and rust we know if a method can fail by looking at it's signature.
  • As with go and rust (and to some extend c) we can have complex error types that can contain further information about the error.
  • As with go and rust the compiler checks proper handling of exceptions.
  • Passing exceptions upwards the call stack happens automatically.

Drawbacks:

  • Java introduces a special syntax for error handling which is adding complexity to the language. Go and rust avoid this by reusing programming constructs that are used elsewhere in the language.
  • It is not clear which line in a piece of code may throw by only looking at how it's used.

For last drawback consider the following code:

WhoThrows.java
import static Example.method1;
import static Example.method2;
import static Example.method3;

public class WhoThrows {
	public static void main(String[] args) throws MyThrowable {
		method1();
		method2();
		method3();
	}
}

From reading this piece of code alone it's impossible to say which function may throw and abort the normal execution flow. Go makes this clear by forcing the caller to use the return values. Rust makes this clear by either handling the return value or by explicity appending ? to the function call.

I think this is a medium drawback of java's approach to exceptions. Especially in big methods it's very hard to reason which lines will be executed and which may be skipped.

Unchecked Exceptions

Java handles a lot of programming errors by throwing exceptions. Examples for this are:

  • accessing a null value.
  • trying to read an array with an out of bounds index
  • dividing an an integer value by zero.

Especially the first two exceptions could appear in almost every line. Forcing the programmer to declare these Exceptions everywhere would only clutter the code without making it more clear. It is also not reasonable to except the programmer to be able to recover from an exception that occures because of a null access since this most likely means that there is a bug in the code. This makes everything else beyond this point unreliable.

The solution for this in java are the so called unchecked exceptions. The compiler doesn't check if these exceptions are handled or declared in the method signature. Otherwise they behave the same. They still could be handled by being caught and they still propagate upwards if they aren't.

Unchecked exceptions are not a unique privilege of the java creators. Everyone can create an unchecked exception by letting it extend RuntimeException.

Which exception type to use and why is it unchecked?

The general guideline is to use checked exception when the caller can be expected to recover from the problem. (See also Unchecked Exceptions — The Controversy by Oracle.)

Springs rest and jdbc abstractions only throw unchecked exceptions. Robert C. Martin writes the following

The debate is over. For years Java programmers have debated over the benefits and liabilities of checked exceptions. When checked exceptions were introduced in the first version of Java, they seemed like a great idea. The signature of every method would list all of the exceptions that it could pass to its caller. Moreover, these exceptions were part of the type of the method. Your code literally wouldn’t compile if the signature didn’t match what your code could do.

At the time, we thought that checked exceptions were a great idea; and yes, they can yield some benefit. However, it is clear now that they aren’t necessary for the production of robust software. C# doesn’t have checked exceptions, and despite valiant attempts, C++ doesn’t either. Neither do Python or Ruby. Yet it is possible to write robust software in all of these languages. Because that is the case, we have to decide—really—whether checked exceptions are worth their price.

Robert C. Martin "Clean Code: A Handbook of Agile Software Craftmanship" (p. 106)

Why are these projects and people advocating against checked exceptions? Let's take a look at the arguments.

Often they can't be handled

Despite the general guideline, to only use checked exceptions when the caller can recover from them, many argue that this can lead to the following problem:

An often cited exception handling best practice is "throw early catch late". What this means is that an exception should propagate upwards until it arrives at an layer in which it can be reasonably handled.

This best practice applied to projects implementing mostly business logic leads to it just propagating most exceptions upwards. These projects tend to be designed in a similar manner:

+-------------------------+
| Framework               |
| +---------------------+ |
| |                     | |
| | BUSINESS            | |
| | LOGIC !!!           | |
| |                     | |
| | (This is the code   | |
| |  we get payed for)  | |
| |                     | |
| | +-----------------+ | |
| | | Persistence etc.| | |
| | +-----------------+ | |
| +---------------------+ |
+-------------------------+

Only very few exceptions really concern the business layer. Most exceptions are just low-level ones for which we don't care. Examples are

  • There is a problem with the DB (no connection, wrong credentials, full disk)
  • There is a problem when trying to access a resource via http
  • A http request may be illformed
  • The client looses it's connection while we write an http response

All these are things that we are not able to handle in our business logic. As a result our code ends up cluttered with exception propagation in the form of throws declarations or wrapping the exceptions in RuntimeExceptions.

They don't work when extending JDK (or other 3rd party) classes

We cannot use the the functional interfaces of java. Supplier,Function,Consumer and so on all don't support checked exceptions. We could write our own variants that are allowed to throw. But than we wouldn't be able to use those with standard library calls like 'Arrays.sort', map or filter.

The Runnable interface has the same problem and it's part of Java since 1.0.

The only way to solve this problem is by wrapping the checked exceptions in unchecked ones. Or to not throw checked ones in the first place...

It's also not only functional interfaces that have these problems. Want to implement your own List class thats backed by a database or file storage? Better wrap all your checked exceptions in unchecked ones to allow the code to compile.