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
