JVM / Java / Exceptions / Checked vs Unchecked

1. Checked Exceptions

Checked exceptions (subclasses of Exception, excluding RuntimeException) must be either caught or declared with throws in the method signature -- the compiler enforces this. They represent conditions a well-written program should anticipate and recover from, like IOException when a file might not exist. This forces callers to consciously decide how to handle the failure.
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

class CheckedExceptions {
    public static void main(String[] args) {
        
        try {
            readFile("data.txt");
        } catch (IOException e) {
            System.out.println("Could not read the file: " + e.getMessage());
        }
    }

    static void readFile(String path) throws IOException { // must declare, or catch internally
        Files.readString(Path.of(path));
    }
}

/*
    Could not read the file: data.txt
*/

2. Unchecked Exceptions

Unchecked exceptions extend RuntimeException and are not enforced by the compiler -- no throws declaration or catch block is required, though either is still allowed. They usually represent programming errors (NullPointerException, IllegalArgumentException) that could have been prevented, rather than conditions to recover from at runtime, so forcing every caller to handle them would just add noise.
 
class UncheckedExceptions {
    public static void main(String[] args) {

        withdraw(-10);  // compiles fine without a try/catch, throws at runtime if reached
    }

    static void withdraw(double amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");  // no throws needed
        }
    }
}

/*
Exception in thread "main" java.lang.IllegalArgumentException: Amount cannot be negative
        at UncheckedExceptions.withdraw(UncheckedExceptions.java:9)
        at UncheckedExceptions.main(UncheckedExceptions.java:4)
*/

3. Error vs Exception

Throwable has two main subclasses: Exception (recoverable conditions, the ones application code handles) and Error (serious problems like OutOfMemoryError or StackOverflowError that a program usually shouldn't try to catch or recover from, since the JVM itself may be in an unstable state). Catching Throwable or Error directly is almost always a mistake.
 
public class ErrorsExceptions {
    public static void main(String[] args) {

        try {
            recurse();  
            // eventually throws StackOverflowError
        
        } catch (Exception e) {
            /*
                Will NOT execute
                Does NOT catch StackOverflowError
                Error is a sibling of Exception, not a subclass
            */
            System.out.println("Handle Exception: " + e);
        }

        System.out.println("Program continues...");
    }

    public static void recurse() {
        recurse(); // infinite recursion
    }
}

/*
Exception in thread "main" java.lang.StackOverflowError
    at ErrorsExceptions.recurse(ErrorsExceptions.java:15)
    at ErrorsExceptions.recurse(ErrorsExceptions.java:15)
    at ErrorsExceptions.recurse(ErrorsExceptions.java:15)
    at ErrorsExceptions.recurse(ErrorsExceptions.java:15)
    ...
*/