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 timeThe 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)beforecatch (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);
}finallyalways runs — aftertry, after anycatch, even when the block returns, and even when an exception is being thrown. The only things that skip it areSystem.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()); // 2And 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()); // 1The 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
}
}throwsin a method signature declares what callers must handle. It can list several.throwis 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
Exceptionis checked exceptRuntimeExceptionand its subclasses. - Checked = a recoverable external condition; unchecked = a programming error.
Erroris the JVM failing — do not catch it.- Specific
catchclauses must come before broader ones; multi-catch shares one handler. finallyalways runs except onSystem.exit(). Neverreturnfrom it.- try-with-resources closes
AutoCloseableresources in reverse order and keeps suppressed exceptions. throwsdeclares;throwraises.- Always pass the cause when wrapping an exception.