When code fails at runtime
Division by zero. Reading past the end of an array. Calling a method on a reference that points at nothing. Each one is a runtime failure, and in a language without exceptions a single such fault would tear the whole program down with no chance to recover.
Java models these failures as objects. When something goes wrong, the JVM constructs an exception object carrying a short message (such as / by zero) and a record of where it happened, then searches for code willing to handle it. Taking part in that search is exception handling, and the try, catch, and finally keywords are how you do it.
The reward is resilience: a program can notice a failure, clean up, and keep serving the next request instead of dying midway through one.
flowchart TD T["Throwable"] --> ER["Error<br/>do not catch these"] T --> EX["Exception"] EX --> RT["RuntimeException<br/>unchecked"] RT --> A["ArithmeticException"] RT --> N["NullPointerException"] RT --> I["IndexOutOfBoundsException"] EX --> C["IOException<br/>checked"] style RT fill:#c2410c,color:#fff
A family of failure objects
The objects thrown at runtime form a family tree, all descended from Throwable. Knowing the branches tells you what a given catch will and will not match.
One branch is Error, the failures of the JVM itself, such as exhausting memory. You are not expected to catch these; they signal that the environment is broken, not that your method can recover.
The other branch is Exception, and it splits in two. RuntimeException and its subclasses are unchecked: ArithmeticException for impossible maths, NullPointerException for a bare reference, IndexOutOfBoundsException for an out of range position, NumberFormatException for a string that will not parse. The compiler does not force you to guard them, because they usually point at a bug in your own code.
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0; // throws ArithmeticException
System.out.println(result); // never reached
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getMessage());
} finally {
System.out.println("Done with attempt.");
}
}
}
How the three blocks fit together
A try block marks code that might fail, and the JVM runs it statement by statement. If every statement succeeds, control falls straight through the try, skips every catch, runs finally, and continues after the whole construct.
If a statement throws instead, the JVM abandons the try at that exact line — nothing below it runs — and walks down the catch clauses hunting for one whose type matches the exception. The first match wins; its body runs, the exception is considered handled, and the program resumes after the last catch.
finally is the third act and it is unconditional. Whether the try succeeded, threw and was caught, or threw uncaught, finally still runs. That makes it the natural home for cleanup such as closing a file or releasing a lock, work that must happen regardless of outcome.
flowchart TD
S["enter try block"] --> R{"exception thrown?"}
R -->|"no"| TRY["run try body to completion"]
TRY --> FIN["finally runs"]
R -->|"yes"| MATCH{"a catch matches?"}
MATCH -->|"yes"| CATCH["run matching catch"]
MATCH -->|"no"| PROP["exception propagates"]
CATCH --> FIN
PROP --> FIN
FIN --> AFTER["continue after try/catch"]
style FIN fill:#3776ab,color:#fff
The type is the signal
A catch clause does not read the exception's message to decide whether it applies — it matches on type. catch (ArithmeticException e) will catch an ArithmeticException or any of its subtypes, and nothing else. A different runtime failure sails straight past it.
That is why the variable e exists at all: once you have caught the object, you can ask it things. e.getMessage() returns the short text the JVM packed in, such as / by zero, and printing e directly shows the type name together with that message. The type decides whether you catch it; the message tells you what went wrong.
Exceptions travel up the call stack
When no catch in the current method matches, the exception does not simply disappear. It propagates: the JVM unwinds the current method and repeats the search in the method that called it, then that caller's caller, and so on. If the search climbs all the way to main and still finds no handler, the JVM prints the exception together with its stack trace and halts.
That trace — the at ClassName.methodName(File.java:line) lines you will see in every Java error report — is the record of the frames the JVM unwound on its way out. Reading one bottom to top tells you which chain of calls led to the failure, which is most of what debugging a crash amounts to.
public class Main {
public static void main(String[] args) {
try {
String s = null;
System.out.println(s.length()); // throws NullPointerException
} catch (NullPointerException e) {
System.out.println("null reference");
} catch (RuntimeException e) {
System.out.println("something else broke: " + e.getMessage());
} finally {
System.out.println("cleanup");
}
}
}
Several catches, one try
A single try may list several catch clauses, each naming a different type, and the JVM examines them top to bottom. The first clause whose type matches the thrown exception runs, and every clause below it is skipped.
That ordering carries a sharp trap. Because matching works on the type hierarchy, a broad type listed before one of its own subtypes makes the narrower clause unreachable — the broad one would always win first. Java refuses to compile such code, reporting that the subtype has already been caught. Put specific clauses first and broad ones last, and you will never hit that error.
flowchart LR EX["throw ArithmeticException"] --> C1["catch RuntimeException<br/>listed first, matches it"] C1 -.->|"would also match"| C2["catch ArithmeticException<br/>unreachable: compile error"] style C2 fill:#b91c1c,color:#fff style C1 fill:#c2410c,color:#fff
What does this print? The try body runs without throwing. Type the exact output, four lines.
public class Main {
public static void main(String[] args) {
System.out.println("start");
try {
System.out.println("try");
} catch (ArithmeticException e) {
System.out.println("catch");
} finally {
System.out.println("finally");
}
System.out.println("end");
}
}
No exception is thrown, so the
catchblock is skipped entirely.finallyruns whether or not something threw, so it still prints.Four lines in total: start, the try line, finally, and end.
When does the finally block execute?
finally is unconditional. It runs after a try that finishes normally, after a catch that handled an exception, and even when an exception goes uncaught — in that last case it runs before the exception continues up the call stack.
What does this print? nums has three elements, indexed 0 to 2. Predict the exact output, three lines.
public class Main {
public static void main(String[] args) {
int[] nums = {10, 20, 30};
try {
System.out.println(nums[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("bad index");
} finally {
System.out.println("cleanup");
}
System.out.println("done");
}
}
nums[3]is out of bounds — there is no fourth element.Evaluating
nums[3]throws beforeprintlncan print anything.The exception is caught, then
finallyruns, then the line after the whole block.
What does this print? Several catch clauses are listed. Predict the exact output, two lines.
public class Main {
public static void main(String[] args) {
try {
int x = 10 / 0;
System.out.println(x);
} catch (NullPointerException e) {
System.out.println("null");
} catch (ArithmeticException e) {
System.out.println("math");
} catch (RuntimeException e) {
System.out.println("runtime");
}
System.out.println("after");
}
}
10 / 0throws anArithmeticException, not a null problem.Catch clauses are checked top to bottom; the first whose type matches runs.
NullPointerExceptiondoes not match anArithmeticException, so it is skipped.
You write catch (RuntimeException e) immediately followed by catch (ArithmeticException e). What happens?
Catch clauses match in order and the first compatible type wins. Because ArithmeticException extends RuntimeException, the first clause already catches it, leaving the second unreachable. Java reports that as a compile error rather than silently dead code.
Recap
- An exception is an object that carries a failure;
Throwablesplits intoError, which you do not catch, andException. RuntimeExceptionand its subclasses are unchecked —ArithmeticException,NullPointerException,IndexOutOfBoundsException, and friends.trymarks risky code;catchclauses are scanned top to bottom, and the first matching type wins.finallyruns in every case — success, a caught exception, or an uncaught one — so it is where cleanup belongs.- List specific catch types before broad ones, or the broad clause makes the specific one unreachable and the build fails.
Next you will meet the exceptions the compiler refuses to let you ignore — the checked ones — and the rule that decides what your own methods must declare.