Lesson 09 · 10 min read · Java for placements

Exceptions, checked and unchecked

The hierarchy, why the compiler forces you to handle some exceptions, finally against try-with-resources, and what a return inside finally does.

The checked-versus-unchecked question is asked in almost every Java round, and it is one of the few places where Java made a design choice no other mainstream language copied.

The hierarchy

Throwable
 ├── Error                      — the JVM is in trouble. Do not catch.
 │    ├── StackOverflowError
 │    ├── OutOfMemoryError
 │    └── NoClassDefFoundError
 └── Exception                  — your program's problem
      ├── IOException           ─┐
      ├── SQLException           │  CHECKED — the compiler enforces handling
      ├── ClassNotFoundException─┘
      └── RuntimeException      ─┐
           ├── NullPointerException          │
           ├── ArrayIndexOutOfBoundsException│  UNCHECKED
           ├── ArithmeticException           │
           ├── NumberFormatException         │
           ├── ClassCastException            │
           └── IllegalArgumentException     ─┘

The whole rule in one line: everything under Exception is checked, except everything under RuntimeException. Error is unchecked too, but you are not meant to catch it.

Checked versus unchecked

Checked — the compiler will not let you ignore it. Either catch it or declare it:

// will not compile: unhandled IOException
void read() {
    Files.readString(Path.of("marks.txt"));
}

// option 1: handle it
void read() {
    try {
        String data = Files.readString(Path.of("marks.txt"));
    } catch (IOException e) {
        System.out.println("could not read: " + e.getMessage());
    }
}

// option 2: declare it and make the caller decide
void read() throws IOException {
    String data = Files.readString(Path.of("marks.txt"));
}

Unchecked — the compiler says nothing. You may catch it, but you are not required to:

int[] xs = new int[3];
System.out.println(xs[5]);        // compiles fine, throws at run time

The reasoning, which is what the question is really after

Checked exceptions represent conditions outside your program's control that a caller could reasonably recover from: a file is missing, a network is down, a database rejected a query. You cannot prevent them by writing better code, so the language forces you to have a plan.

Unchecked exceptions represent bugs: a null reference, an index past the end, a bad cast. The right response is to fix the code, not to catch the exception. Forcing a try/catch around every array access would be absurd.

So: checked = recoverable external condition, unchecked = programming error. That single sentence answers the question.

Checked Unchecked
Compiler enforces yes no
Superclass Exception RuntimeException
Means external condition a bug
Example IOException NullPointerException
throws needed yes optional

try, catch, finally

try {
    int result = 10 / divisor;
    System.out.println(result);
} catch (ArithmeticException e) {          // most specific first
    System.out.println("cannot divide by zero");
} catch (RuntimeException e) {             // wider
    System.out.println("something else: " + e.getMessage());
} finally {
    System.out.println("always runs");
}

Rules that get asked:

  • Order matters. A more specific exception must come before a broader one. catch (Exception e) before catch (IOException e) is a compile error — the second is unreachable.
  • Multi-catch since Java 7, when the handling is the same:
try {
    risky();
} catch (IOException | SQLException e) {    // e is effectively final here
    log(e);
}
  • finally always runs — after try, after any catch, even when the block returns, and even when an exception is being thrown. The only things that skip it are System.exit() and the JVM dying.

The finally return trap

A classic "what does this print" question:

static int test() {
    try {
        return 1;
    } finally {
        return 2;          // overwrites the return value
    }
}
System.out.println(test());        // 2

And the subtler version:

static int count() {
    int x = 1;
    try {
        return x;          // the VALUE 1 is captured here
    } finally {
        x = 99;            // too late — the return value was already fixed
    }
}
System.out.println(count());       // 1

The takeaway is practical: never return from a finally block. It silently discards both return values and in-flight exceptions, and it is the kind of thing that hides a bug for years.

try-with-resources

Closing a resource in finally is verbose and easy to get wrong:

BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("marks.txt"));
    System.out.println(br.readLine());
} catch (IOException e) {
    log(e);
} finally {
    if (br != null) {                 // null check needed — the constructor may have thrown
        try {
            br.close();               // close() itself throws IOException
        } catch (IOException ignored) { }
    }
}

Java 7 replaced all of that:

try (BufferedReader br = new BufferedReader(new FileReader("marks.txt"))) {
    System.out.println(br.readLine());
} catch (IOException e) {
    log(e);
}

Anything implementing AutoCloseable can go in the parentheses, and it is closed automatically — in reverse order of declaration, and even if the body throws. Multiple resources are separated by semicolons:

try (var in = new FileReader("a.txt");
     var out = new FileWriter("b.txt")) {
    out.write(in.read());
}

One further advantage worth mentioning: if the body throws and close() throws, the manual version loses the original exception; try-with-resources keeps it and attaches the second as a suppressed exception, reachable via getSuppressed().

throw and throws

Two keywords, one letter apart, completely different:

void validate(int marks) throws InvalidMarksException {   // DECLARES what may come out
    if (marks < 0) {
        throw new InvalidMarksException("negative: " + marks);   // THROWS one now
    }
}
  • throws in a method signature declares what callers must handle. It can list several.
  • throw is a statement that raises an exception object immediately.

Custom exceptions

Extend Exception for checked, RuntimeException for unchecked:

public class InvalidMarksException extends Exception {      // checked
    private final int marks;

    public InvalidMarksException(String message, int marks) {
        super(message);
        this.marks = marks;
    }

    public int getMarks() { return marks; }
}

public class RosterCorruptException extends RuntimeException {   // unchecked
    public RosterCorruptException(String message, Throwable cause) {
        super(message, cause);        // keep the original — do not discard it
    }
}

That Throwable cause parameter is worth using. Wrapping a low-level exception without the cause throws away the stack trace that tells you what actually went wrong:

try {
    parse(line);
} catch (NumberFormatException e) {
    throw new RosterCorruptException("bad row: " + line, e);    // cause preserved
}

Choose checked when the caller can genuinely do something about it; unchecked when it means the program is wrong. When in doubt, most modern Java leans unchecked — but be ready to defend either answer, because interviewers hold both opinions.

The methods on an exception

catch (Exception e) {
    e.getMessage();              // the text you passed to the constructor
    e.getCause();                // the wrapped exception, or null
    e.printStackTrace();         // to stderr — fine for a demo, not for production
    e.getStackTrace();           // StackTraceElement[] — programmatic access
    e.getClass().getName();      // the type
}

Use a logger rather than printStackTrace() in anything real: printStackTrace writes to standard error with no timestamp, no level and no context.

What to take into an interview

  • Everything under Exception is checked except RuntimeException and its subclasses.
  • Checked = a recoverable external condition; unchecked = a programming error.
  • Error is the JVM failing — do not catch it.
  • Specific catch clauses must come before broader ones; multi-catch shares one handler.
  • finally always runs except on System.exit(). Never return from it.
  • try-with-resources closes AutoCloseable resources in reverse order and keeps suppressed exceptions.
  • throws declares; throw raises.
  • Always pass the cause when wrapping an exception.
Lesson 9 of 12 · all 12 are free to read, no account. Course contents