V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 11 of 20

Exception Handling

checked vs unchecked, try-with-resources, exception chaining, custom exceptions

When to Use Exceptions

Exceptions express "unexpected but needs handling" situations: file read failure, network disconnect, DB connection timeout, invalid user input, JSON parse error. Java designs them as throwable/catchable objects rather than return values, so the caller can handle them in place, propagate them up, or catch them all at a top level.

  • ✅ Good cases: IO failure / network error / resource unavailable / business validation failure / missing config
  • ❌ Bad cases: using exceptions for control flow (e.g. using NumberFormatException to test whether a string is a number)
  • Rule: if "errors" are a normal part of the flow (like form validation), consider a return value / Optional; only throw for true "exceptions"

throw and throws

throw raises an exception object inside a method; throws appears in the method signature, declaring which checked exceptions the method may throw and forcing the caller to either catch or keep declaring throws.

// ThrowDemo.java
public class ThrowDemo {
    static int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("divide by zero");
        }
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(divide(10, 2));
        try {
            System.out.println(divide(10, 0));
        } catch (ArithmeticException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}

checked vs unchecked

Java splits exceptions in two: checked must be handled or declared with throws, enforced by the compiler; unchecked (RuntimeException and subclasses, Error) are not enforced.

  • checked: IOException, SQLException, ClassNotFoundException, etc. — common in library APIs, forcing callers to face them
  • unchecked (the RuntimeException family): NullPointerException, IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException — mostly "programmer errors"
  • Error: OutOfMemoryError, StackOverflowError — JVM-level failures, don't catch them
// CheckedVsUnchecked.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedVsUnchecked {
    // must throws or handle in place (checked)
    static String readConfig(String path) throws IOException {
        return Files.readString(Path.of(path));
    }

    // no declaration needed (unchecked)
    static int parseAge(String s) {
        int n = Integer.parseInt(s); // parse failure throws NumberFormatException
        if (n < 0) throw new IllegalArgumentException("age < 0");
        return n;
    }

    public static void main(String[] args) throws IOException {
        System.out.println(parseAge("30"));
        try {
            parseAge("abc");
        } catch (NumberFormatException e) {
            System.out.println("bad number: " + e.getMessage());
        }
    }
}

try-catch-finally and Multi-catch

A finally block runs whether or not an exception is thrown, commonly used for resource cleanup (the old way). Multi-catch combines with `|`: handle several exceptions the same way.

// TryCatch.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class TryCatch {
    public static void main(String[] args) {
        try {
            String content = Files.readString(Path.of("/no/such/file"));
            System.out.println(content);
        } catch (NoSuchFileException e) {
            System.out.println("file missing: " + e.getFile());
        } catch (IOException | RuntimeException e) {
            // handle several exceptions together
            System.out.println("other error: " + e);
        } finally {
            System.out.println("cleanup runs anyway");
        }
    }
}

try-with-resources (Essential for IO / DB)

Anything "must be closed once opened" — file streams, DB connections, HTTP clients, sockets — should use try-with-resources. An object declared in the parentheses, as long as it implements AutoCloseable, is close()d automatically when leaving the try block — shorter and safer than try-finally (avoids forgetting to close, avoids close() throwing and swallowing the original exception).

// TryWithResources.java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class TryWithResources {
    public static void main(String[] args) throws IOException {
        Path p = Files.createTempFile("demo", ".txt");
        Files.writeString(p, "line1\nline2\nline3");

        // multiple resources are separated by semicolons; closed in reverse declaration order
        try (BufferedReader r = Files.newBufferedReader(p)) {
            String line;
            while ((line = r.readLine()) != null) {
                System.out.println(line);
            }
        }
        // by here, r has already been closed automatically

        Files.delete(p);
    }
}

Exception Chaining: Wrap, Don't Swallow

When a low-level exception is unfriendly to the upper layer (e.g. exposing SQLException to a Controller is inappropriate), wrap it to keep the low-level exception as the cause. Then throw new XxxException("business note", e) — both the call stack and the original exception show up in logs.

// ExceptionChain.java
public class ExceptionChain {
    static String loadUser(long id) throws UserNotFoundException {
        try {
            // pretend this is a SQL query
            if (id <= 0) throw new RuntimeException("SQL error: bad id");
            return "Alice";
        } catch (RuntimeException dbErr) {
            throw new UserNotFoundException("load user " + id + " failed", dbErr);
        }
    }

    public static void main(String[] args) {
        try {
            loadUser(-1);
        } catch (UserNotFoundException e) {
            System.out.println(e.getMessage());
            System.out.println("caused by: " + e.getCause());
            // e.printStackTrace(); // full chain
        }
    }
}

class UserNotFoundException extends Exception {
    public UserNotFoundException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

Custom Exceptions

The business layer often needs its own exception types so callers can catch by type. Convention: business exceptions usually extend RuntimeException (unchecked, to avoid polluting signatures with throws everywhere), named ending in Exception.

// CustomException.java
public class CustomException {
    public static void main(String[] args) {
        try {
            validate("");
        } catch (ValidationException e) {
            System.out.println(e.getField() + ": " + e.getMessage());
        }
    }

    static void validate(String name) {
        if (name == null || name.isBlank()) {
            throw new ValidationException("name", "must not be blank");
        }
    }
}

class ValidationException extends RuntimeException {
    private final String field;
    public ValidationException(String field, String message) {
        super(message);
        this.field = field;
    }
    public String getField() { return field; }
}

Practical Points

  • Never write an empty catch block; at least log it
  • Don't catch Exception, do nothing, then throw a new exception with no cause — that loses the stack trace
  • Throw RuntimeException for business-flow failures; use checked only when a library must force the caller to handle it
  • When a low-level exception is meaningless to the upper layer, wrap it: throw new XxxException("msg", originalException)
  • Always replace hand-written finally close with try-with-resources
  • Frameworks like Spring almost always convert checked to unchecked — the mainstream trend is to use checked sparingly