Exceptions for Error Handling

Normal

public static void main(String[] args) {
    FileReader file = new FileReader(args[0]);
    List<Point> points = new Scanner(file).useDelimiter("\n")
        .tokens()
        .map(str -> new Scanner(str))
        .map(sc -> new Point(sc.nextDouble(), sc.nextDouble()))
        .toList();

    DiscCoverage maxCoverage = new DiscCoverage(points);
    System.out.println(maxCoverage);
}

1 - Catching

Deal with it within the body

try {
    FileReader file = new FileReader(args[0]);
    List<Point> points = new Scanner(file).useDelimiter("\n")
        .tokens()
        .map(str -> new Scanner(str))
        .map(sc -> new Point(sc.nextDouble(), sc.nextDouble()))
        .toList();
    DiscCoverage maxCoverage = new DiscCoverage(points);
    System.out.println(maxCoverage);
} catch (FileNotFoundException ex) {
    System.err.println("Unable to open file " + args[0] + "\n" + ex);
}

Multiple Exceptions

In any try-catch system, throw always exist, either explicitly or hidden inside a library method.

try {
    FileReader file = new FileReader(args[0]);
    List<Point> points = new Scanner(file).useDelimiter("\n")
        .tokens()
        .map(str -> new Scanner(str))
        .map(sc -> new Point(sc.nextDouble(), sc.nextDouble()))
        .toList();
    DiscCoverage maxCoverage = new DiscCoverage(points);
    System.out.println(maxCoverage);
} catch (FileNotFoundException ex) {
    System.err.println("Unable to open file " + args[0] + "\n" + ex);
} catch (ArrayIndexOutOfBoundsException ex) {
    System.err.println("Missing filename");
} catch (NoSuchElementException ex) { // includes InputMismatchException
    System.err.println("Incorrect file format\n");
} finally {
    System.out.println("Program Terminated\n");
}

Multiple catch blocks ordered by most specific exceptions first. If you placed broader catches shadowing a specific one, there is actually a compile error.

finally runs always, regardless of whether an exception was thrown or caught. Success or failure, finally executes.

2 - Throwing

Throw: moves up the chain, someone else will deal with it (better if others have more context)

public static void main(String[] args) throws FileNotFoundException {
	...
	}
Circle createUnitCircle(Point p, Point q) {
    double distPQ = p.distanceTo(q);
    if (distPQ < EPSILON || distPQ > 2.0 + EPSILON) {
        throw new IllegalArgumentException("Distance pq not within (0, 2]");
    }
    ...
    return new Circle(...);
}

Note: throw without a catch in the same method means it propagates up to whoever called createUnitCircle. That caller is responsible for catching it.


Creating your own exceptions

class IllegalCircleException extends IllegalArgumentException {
    IllegalCircleException(String message) {
        super(message);
    }

    @Override
    public String toString() {
        return "IllegalCircleException:" + getMessage();
    }
}

IllegalCircleException extends IllegalArgumentException means IlliegalCircleException is a IllegalArgumentException, with a more specific name and custom toString.

The constructor is just to be a constructor. The usecase here is to customise the toString message for a specific exception.


Normal vs Exception Control Flow

Normal Control Flow: a flow without any exceptions. Exception Control Flow: a flow with an exception.

Why need throw when we have catch? m4 is a low-level method. It just throws an error. The catch intercepts it.


Types of Exceptions

Checked — is one that the programmer is expected to actively anticipate and handle. FileNotFoundException. Anticipated, recoverable situations. All checked exceptions to be caught or thrown.

Unchecked — Java doesn’t force you. ArithmeticException, IllegalArgumentException. Usually bugs, not recoverable.

Things to take note of:

  • When overriding a method that throws a checked exception, the overriding method cannot throw a more general exception.
  • Avoid Pokemon Exception Handling, catch (Exception ex) This means catching everything.
  • Don’t just throw everything. Handle it at the level that has the context to deal with it. Handle exceptions at the appropriate abstraction level, do not just throw and break the abstraction barrier.

Pure Functions or Pure Fantasy

Don’t handle the side effect directly, wrap it in a context that manages it for you.

  • Maybe/Optional is the context for handling missing values
  • Lazy is the context for handling delayed data with caching
  • Stream is the context for handling (infinite) looping, as well as for parallel processing
  • Try is the context for exception handling

Functor — anything that has map. Optional.map, Stream.map — both functors. The contract is: apply a function (whatever inside the map) to the value inside the context, return the same type of context wrapping the result.

Monad — anything that has flatMap on top of that / a functor with flatMap. The contract is: apply a function that itself returns a wrapped value, and don’t double-wrap it.


Using Try to handle exceptions (pipelining)

Try.of(() -> 1)
// ==> Success: 1

Try.of(() -> 1).map(x -> x + 2)
// ==> Success: 3

Try.of(() -> 1).map(x -> x + 2).tryCatch(() -> 0)
// ==> 3

Try.of(() -> 1 / 0).map(x -> x + 2).tryCatch(() -> 0)
// ==> 0

Try is exactly like Optional but for exceptions. Instead of empty vs present, you have Failure vs Success.

Try.of wraps a computation. If it succeeds, you get Success. If it throws, you get Failure with the exception stored inside. map only runs if it’s a Success — if it’s already a Failure, it skips through. tryCatch is like orElse — gives you a default if it’s a Failure.

Try Interface

Notice how the try-catch is hidden inside of

interface Try<T> {
    static <T> Try<T> of(Supplier<? extends T> supplier) {
        try {
            // supplier needs to be exception free to create succ
            return succ(supplier.get());
        } catch (Exception ex) {
            return fail(ex);
        }
    }
    
    T tryCatch(Supplier<? extends T> supplier);

    <R> Try<R> map(Function<? super T, ? extends R> mapper);

    <R> Try<R> flatMap(Function<? super T, ? extends Try<? extends R>> mapper);

	// can't be accessed
    private static <T> Try<T> succ(T t) { 
        return new Success<T>(t);
    }

    private static <T> Try<T> fail(Exception ex) {
        return new Failure<T>(ex);
    }
}

Identity and Associativity Law of the Functor

Functor: anything with map that applies a function Law: (ftr.map(f)).map(g) ≡ ftr.map(g.compose(f))

// Identity Law
a + 0 = a

Function<Integer, Try<Integer>> f = x -> Try.of(() -> x);
Function<Integer, Try<Integer>> e = x -> Try.of(() -> x / 0);

f.apply(5)              // Success: 5
f.apply(5).map(x -> x) // Success: 5

e.apply(5)              // Failure: ArithmeticException: / by zero
e.apply(5).map(x -> x) // Failure: ArithmeticException: / by zero
// Associativity Law
(a + b) + c = a + (b + c)

Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> mulTwo = x -> x * 2;

f.apply(5).map(addOne).map(mulTwo)              // Success: 12
f.apply(5).map(mulTwo.compose(addOne))          // Success: 12
f.apply(5).map(x -> mulTwo.apply(addOne.apply(x))) // Success: 12

err.apply(5).map(addOne).map(mulTwo)            // Failure: ArithmeticException: / by zero
err.apply(5).map(mulTwo.compose(addOne))        // Failure: ArithmeticException: / by zero

Identity and Associativity Law of the Monad

// Identity Law
a + 0 = a

Function<Integer,Try<Integer>> id = x -> Try.of(() -> x);

f.apply(5)              // Success: 5
f.apply(5).flatMap(id)  // Success: 5  (right identity)

e.apply(5)              // Failure: / by zero
e.apply(5).flatMap(id)  // Failure: / by zero  (failure propagates)

id.apply(5).flatMap(f)  // Success: 5  (left identity)
id.apply(5).flatMap(e)  // Failure: / by zero
// Associativity Law
(a + b) + c = a + (b + c)

Function<Integer,Try<Integer>> addOne = x -> Try.of(() -> x + 1);
Function<Integer,Try<Integer>> mulTwo = x -> Try.of(() -> x * 2);

// These two are equivalent:
f.apply(5).flatMap(addOne).flatMap(mulTwo)          // Success: 12
f.apply(5).flatMap(x -> addOne.apply(x).flatMap(mulTwo))  // Success: 12

// NOTE: mulTwo.compose(addOne) is a COMPILE ERROR
// addOne returns Try<Integer>, but mulTwo expects Integer
// So use: x -> addOne.apply(x).flatMap(mulTwo)

Right vs Left Identity

Right identity: mnd.flatMap(x -> unit(x)) ≡ mnd — unit is on the right (inside the flatMap)
Left identity: unit(a).flatMap(f) ≡ f(a) — unit is on the left (wrapping the starting value)

Same idea as addition:

a + 0 = a → right identity (0 on the right)
0 + a = a → left identity (0 on the left)

unit is just the "0" of flatMap.

The rationale

For identity, it just ensures there’s no side effects from wrapping / unwrapping. For associativity, it’s useful to keep in mind when doing pipelining.

Laws