Demystifying Exception Handling in Java (Scratchpad Notes)

Analogy for Java try catch block –

Java try block is used to enclose the code that might throw an exception. It must be used within the method. Java try block must be followed by either catch or finally block.

Thinking with anology: Think of the Java try block as a safety net that catches any errors (exceptions) that might occur in your code, like a safety net catching acrobats in a circus performance.

Checked Exception aka Compile time enforced Exception –

  • A checked exception must be handled either by re-throwing or with a try catch block, whereas an unchecked isn’t required to be handled.
  • Checked exceptions are also known as compile-time exceptions as these exceptions are checked by the compiler during the compilation process to confirm whether the exception is handled by the programmer or not. If not, then the system displays a compilation error.
  • checked exception => Java compiler checks at compile time if this exception has been handled or not. If not, a compile-time error occurs.

Unchecked Exception aka Runtime Exception –

  • Unchecked Exceptions are often result of programming errors e.g. NPE, IOOBE
  • All unchecked exceptions are runtime exceptions. . Runtime exceptions are ignored at the time of compilation. You aren’t required to specify or handle the exception, although you can if you want.
  • Unchecked/run-time exceptions are generally reserved for issues that are often results of programming errors. Since a missing file could be due to external factors outside the program’s control, FileNotFoundException seems to fit better in the “unchecked” category.

FAQs:
When do you use a Checked exception over Unchecked exception ?
If you want to enforce the developer to handle an exception you declare the exception as a checked exception so compiler will ensure that developer has handled the exception.
Because, If an exception is a checked exception, it means that it need to be declared in method signatures.

Your library Exception hierarchy can look like –

root exception class for a type e.g. UserException – The root class for all defined user exceptions.

protected constructor() – This constructor can only be called by subclasses.

Exception class hierarchy:

Exception <- Throwable <- Object
Exception is a Throwable.

Methods inherited from class java.lang.Throwable which is a superclass for Exception:
addSuppressed, fillInStackTrace, getCause, getLocalizedMessage, getMessage, getStackTrace, getSuppressed, initCause, printStackTrace, printStackTrace, printStackTrace, setStackTrace, toString

Why we would want FileNotFoundException to be a checked –

  1. Enforced Error Handling.
  2. Recoverability – Checked exceptions promote recoverability. Some programming languages handle file-related errors with unchecked exceptions. There’s a degree of design philosophy involved in this decision. Java prioritizes the benefits listed above, even if it means slightly more verbose code.

“Stuff that goes wrong” comes in two basic flavours: recoverable, and unrecoverable… Ergo: Business Exceptions and System Errors.

Two types of developers:

  1. library developer.
  2. application programmer who uses libraries.

IOException is a “catch all” checked exception. why ?

Philosopy on why Java has both checked and unchecked exception:
https://stackoverflow.com/questions/1074358/why-does-java-have-both-checked-and-unchecked-exceptions

When an exception happens –

  1. you can let it propagate i.e. intentionally bubble up: propogate by 1. not handling it or 2. handling it by let’s say log it and re-throw it. If you let it propagate silently you might have lost context in the block where the exception happened.
  2. you can handle it at source. You will likely have specific context in this block to log for debugging purposes.

un-intentionally bubbling up should be fixed in your code because –

  1. as you might loose context at the source when it bubbles up and handled very lates.
  2. Long stack traces that are hard to read.

Understanding checked exception with anology:

When designing an interface, think of exceptional cases that can occur, and will occur, with the normal state of a method call. Declare these exceptions in your interface, as the programmer will have to handle them directly.

For example, a bank account withdraw method may declare an OverdraftException, which is an expected exception – a withdrawal may fail due to overdraft, but this type of failure may be handled differently by the client code (one may decide to completely deny the withdrawal, another may decide to apply a huge penalty and allow for a negative balance to be recorded, another may decide that their client is allowed to draw from a different account).

In case of run-time exceptions –

Developer can choose to handle it or developer can choose to let it propagate and be logged as an error. Default is: propagates and so not noob friendly to handle expceptions but a pain in ass for experts as all they want is to let is propagate and don’t want to write boiler plate code to let it propagate.

generally you cannot do anything but log that an error occur

Have a root exception for a class of exception and child/subclass exceptions with Exception names that should clearly show what happened when they are called.

e.g.
UserException -> ObjectNotFoundException -> DomainNotFoundException
SystemExcepiton -> OOMError

UserException in this case is a defined Root Exception
DomainNotFoundException clearly says we couldn’t find Domain which is a object not found and more clearly a user exception as the user called with a exception that doesn’t exist.

SQLException is a bad checked exception. Exception names should show what happened when they are called. SQLException does not. SQLException is thrown any single time any possible number of errors are encountered when dealing with a database – MOST OF WHICH THAT HAVE NOTHING TO DO WITH SQL.

Pros of checked exceptions –

  1. idiot proof – A fool may understand why SQLException could happen and might right some intelligent code to log it appropriately.

Cons of checked exceptions –

  1. Since programmers are forced to handle the programmer might ignorantly ignore the exception leading to errors never being surfaced and silently eaten up causing serious inconsistencies.
  2. Introduces a lot of boiler plate code: Boiler plate code to handle checked exceptions and could bubble up too far in the chain from interface in a library to several layers on top say 100th chain.

Pros of unchecked/run-time exceptions –

  1. It forces errors to show up and then you must fix the code directly instead of ignoring the exception. Then the developer will be writing an exception handler that they were forced to write because of a real problem – unlike checked exception where because a method signature said that it might possibly throw an Exception.
  2. Clean code: as it avoids unnecessary boiler plate code by not forcing you to handle an exception. Say if you expect a DataAccessException (runtime exception), you can handle it.

Cons of unchecked/run-time exceptions –

  1. Not idiot proof – Where Spring uses a DataAccessException to wrap SQLExceptions and the noob develoepr using Spring for Database connectivity likely not handle the run-time exception resulting in a large stack trace missing the context of exception and debuggability due to a large stack trace.

Spring uses a DataAccessException to wrap SQLExceptions so that you don’t have to handle them as a checked exception.

SQLException which is a checked exception eats a lot of exceptions –

  1. exceptions of type SQL errors like bad SQL query.
  2. exceptions of type where access to data failed.

So Spring wrap of SQL Exception as DataAccessException is more preferrable because all true SQL exceptions would be caught during development and testing (unless runtime query building with bad SQL query) and in production it is highly likely an issue to access the data from database like say caused by database connection/database health/network to database issue i.e. not that your SQL query was nessecarily at fault.

Final Takeaways:

  1. When appropriate, you can always wrap a checked exception as unchecked/runtime exception and re-throw so you don’t have to bubble up checked exception further up.
  2. Viceversa, when appropriate, if you want your code to explicitly handle these scenarios, you can always wrap these runtime exceptions with your checked exceptions and rethrow them.
  3. If you don’t handle an exception locally i.e. in the block scope where exception happened – you are abruptly handling flow of exception and rethrowing the exception to one level up in the call stack.
  4. Use user defined specific exceptions over user defined exceptions
  5. Generic exceptions that eat a lot of types/classes of exceptions is bad e.g. IOException and SQLException.

Why SQLException should have been unchecked exception:

  1. Most SQLExceptions are unexpected/runtime in scope so making low-level, extremely rare exceptions checked is what makes very little sense.

IOException is a superclass for even Network related exception and making it a checked exception could have made Server handle retries. In this case it should been shamelessly propagated and passed to client and client can determine what to do – retry / fallback to another provider etc. Bubbiling up several runtime exceptions into one generic IOException is a nonsense for developers as it is difficult to figure out how to handle it.

Case Study:
FileNotFoundException is a checked exception even though it happens at runtime and in most cases there is no recovery possible. Shouldn’t it be a unchecked exception ?

Why your reasoning makes sense:

Runtime Occurrence: You’re correct that whether a file exists or not can only be definitively determined at runtime. The compiler can’t predict if every possible file path you provide in your code will be valid when the program executes.
Spirit of Unchecked Exceptions: Unchecked exceptions are generally reserved for issues that are often results of programming errors (e.g., NullPointerException, IndexOutOfBoundsException). Since a missing file could be due to external factors outside the program’s control, FileNotFoundException seems to fit better in the “unchecked” category.
Arguments for keeping FileNotFoundException checked:

Enforced Error Handling: Checked exceptions force developers to explicitly consider and handle potential file access problems. This encourages a more robust approach to file I/O and can help avoid unexpected crashes in production environments.
Recoverability: In many cases, a FileNotFoundException is recoverable. The user might be asked to provide a different file path, or the program could implement a default behavior if the file is optional. Checked exceptions promote this kind of structured error handling.
Legacy and Consistency: Changing FileNotFoundException to unchecked now would have ripple effects throughout the Java ecosystem, breaking existing code that relies on the current behavior. Java tends to be conservative in making such fundamental changes.
Could it be different?

Absolutely! Some programming languages handle file-related errors with unchecked exceptions. There’s a degree of design philosophy involved in this decision. Java prioritizes the benefits listed above, even if it means slightly more verbose code.

Example (how to handle it):

Java

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileReadingExample {
public static void main(String[] args) {
File file = new File("my_file.txt");

    try {
        Scanner scanner = new Scanner(file);
        while (scanner.hasNextLine()) {
            System.out.println(scanner.nextLine()); 
        }
        scanner.close();
    } catch (FileNotFoundException e) {
        System.err.println("Error: File not found. Please check the path.");
    }
}

}

Scenario 1: FileNotFoundException (Checked)

  • The Problem: You’ve instructed your Java code to open a file, but, alas, the file cannot be found at the specified path. This could be due to a typo in the filename, the file being deleted, or you trying to access a file on a drive that’s not connected.
  • Why Checked? A missing file is often a condition you might want to recover from. Here’s how forcing you to handle it helps:
    • User Experience: You can prompt the user for a different filename or provide a default file if the original is optional.
    • System Integrity: You have a chance to avoid a crash and potentially save unsaved work or log the error for debugging.

Detailed Example

Java

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileReader {
    public static void main(String[] args) {
        String filename = "my_data.txt"; 
        try {
            readFile(filename);
        } catch (FileNotFoundException e) {
            System.err.println("Error: File not found - " + filename);
            // Add code to recover: ask the user for a new filename, etc. 
        }
    }

    public static void readFile(String filename) throws FileNotFoundException {
        File file = new File(filename);
        Scanner scanner = new Scanner(file);
        // ... Process the file contents ...
        scanner.close();

    }
}

Scenario 2: NullPointerException (Unchecked)

  • The Problem: You’re trying to use an object reference variable that doesn’t actually point to a real object. It’s like trying to use a phone where the phone number is empty.
  • Why Unchecked? This is nearly always a programmer error. You should have initialized variables before using them. The compiler trusts you to avoid this.

Detailed Example

Java

public class MyClass {
    private String message; // Not initialized

    public void printMessage() {
        System.out.println(message.length()); // Uh oh! 'message' is null 
    }
}

Scenario 3: ArrayIndexOutOfBoundsException (Unchecked)

  • The Problem: You’re trying to access an array element using an index that’s either negative or larger than the array’s size. Think of it as trying to find a book on a shelf that doesn’t exist.
  • Why Unchecked? Again, this is generally a programmer error. You’re responsible for ensuring your array accesses stay within the bounds.

Detailed Example

Java

public class ArrayExample {
    public static void main(String[] args) {
        int[] numbers = {10, 20, 30};
        System.out.println(numbers[3]); // Array only has elements 0, 1, and 2
    } 
}

Sources:

  1. https://stackoverflow.com/questions/1074358/why-does-java-have-both-checked-and-unchecked-exceptions

Leave a Reply

Your email address will not be published. Required fields are marked *