Exception Handling
A complete guide to exception handling in Java: the exception hierarchy, checked versus unchecked exceptions, try-catch-finally, try-with-resources, throw and throws, common built-in exceptions, custom exception classes, exception chaining, and multi-catch blocks.
What is an Exception?
An exception is an event that disrupts the normal flow of a program. When something goes wrong at runtime, such as dividing by zero, trying to open a file that does not exist, or calling a method on a null reference, the JVM creates an exception object that describes the problem and hands it off to the exception handling mechanism.
Without exception handling, an unhandled exception terminates the program and prints a stack trace to the console. With it, you can intercept the problem, respond to it gracefully, clean up any resources you were using, and decide whether to continue or to re-signal the problem to the caller with more context.
Error vs Exception: the core distinction
Not every abnormal condition is something your code should try to recover from.
- Error:Signals a serious JVM-level problem that application code generally cannot and should not try to recover from. Examples: OutOfMemoryError, StackOverflowError.
- Exception:Signals a condition that well-written application code may reasonably handle. Examples: IOException, NullPointerException, IllegalArgumentException.
Exception Hierarchy
Every exception in Java is an object. The root of the entire hierarchy is java.lang.Throwable. Only objects that are instances of Throwable can be thrown with the throw keyword or caught with a catch block. Throwable has two direct subclasses: Error and Exception.
The hierarchy laid out
Understanding the tree structure is essential before learning which exceptions to catch and which to declare.
- Throwable:Root. Provides getMessage(), getCause(), getStackTrace(), and printStackTrace().
- Error (extends Throwable):JVM-level failures. Subclasses include OutOfMemoryError, StackOverflowError, and AssertionError. Do not catch these in normal code.
- Exception (extends Throwable):Application-level problems. This is the branch your code normally works with.
- RuntimeException (extends Exception):Unchecked exceptions. NullPointerException, IllegalArgumentException, and ClassCastException live here. The compiler does not force you to handle them.
- All other Exception subclasses:Checked exceptions. IOException, SQLException, ParseException. The compiler forces you to either catch them or declare them with throws.
Exploring the Exception Hierarchy
JavaUsing instanceof to verify where various exceptions sit in the hierarchy.
Checked vs Unchecked Exceptions
Java divides exceptions into two groups based on how the compiler treats them. This distinction is one of the most important things to understand early, because it determines what the compiler will and will not enforce on you.
Checked exceptions
The compiler verifies at compile time that you have dealt with the possibility of a checked exception.
- Any class that extends Exception but does NOT extend RuntimeException is checked.
- If a method can throw a checked exception, it must either catch it with try-catch or declare it in its throws clause. Failing to do either is a compile error.
- Examples: IOException, FileNotFoundException, SQLException, ParseException, ClassNotFoundException.
- The rationale is that these represent external conditions (file system, network, database) that the caller should consciously decide how to handle.
Unchecked exceptions
The compiler does not force you to handle or declare unchecked exceptions.
- Any class that extends RuntimeException, or extends Error, is unchecked.
- You may catch them, but you are not required to. The compiler will not complain if you ignore them.
- Examples: NullPointerException, ArrayIndexOutOfBoundsException, NumberFormatException, ClassCastException, StackOverflowError.
- The rationale is that these typically represent programming bugs (passing null where it is not allowed, accessing an array out of bounds) that should be fixed rather than silently caught and swallowed.
try and catch Block
The try block contains the code that might throw an exception. The catch block follows it and is only executed if the try block throws an exception of the declared type. If no exception is thrown, the catch block is skipped entirely and execution continues normally after the try-catch structure.
The exception object caught by the catch block is a normal Java object. You can call getMessage() to get a description of what went wrong, getClass().getSimpleName() to get the exception type name, and printStackTrace() to print the full call stack to standard error.
Basic try-catch
JavaCatching an ArithmeticException from integer division by zero.
Multiple catch Blocks and Catch Order
A single try block can be followed by multiple catch blocks, each handling a different exception type. The JVM tries them in the order they are written and executes the first one that matches the thrown exception.
The ordering rule is strict: more specific exception types must come before more general ones. If you place a catch for Exception before a catch for NullPointerException, the compiler produces an error because the NullPointerException handler would be unreachable: every NullPointerException is also an Exception, so it would already be caught by the parent handler.
Multiple catch Blocks: Specific Before General
JavaThree catch blocks ordered from most specific to most general to handle each case individually.
finally Block
The finally block runs after the try and catch blocks have finished, regardless of what happened. Whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught, the finally block still runs. This makes it the right place for cleanup code: closing files, releasing database connections, freeing locks, or logging completion.
The only situations where a finally block does not execute are a JVM crash, a call to System.exit(), or the thread being killed forcibly. In all normal circumstances, finally is guaranteed to run.
One subtle trap with finally
Be careful about using return inside a finally block.
- If a finally block contains a return statement, it overrides any return value or exception from the try or catch block. The original result or exception is silently discarded.
- This is almost always a bug. Avoid return, throw, break, and continue inside finally blocks unless you have a very specific reason.
finally Block Execution
JavaDemonstrating that finally runs in all three scenarios: success, caught exception, and uncaught exception.
try-with-resources (Java 7+)
Before Java 7, closing resources reliably required a try-finally block that was often long and error-prone. If the try body and the finally block both threw exceptions, the one from the finally block would silently swallow the original exception. Java 7 introduced try-with-resources to solve this cleanly.
Any class that implements the AutoCloseable interface (which includes Closeable) can be declared in the try parentheses. The JVM guarantees that close() is called on each resource when the block exits, in the reverse order of declaration. If both the try body and the close() method throw exceptions, the close exception is added as a suppressed exception and the original try-body exception propagates, which is the correct behaviour.
try-with-resources
JavaA custom AutoCloseable resource demonstrating automatic cleanup and suppressed exceptions.
throw Statement
The throw statement lets your code deliberately raise an exception. You pass it an instance of any class that extends Throwable. In practice, you almost always throw an Exception subclass, not an Error. Once a throw statement executes, the normal flow stops immediately and the JVM begins looking for a suitable catch block, travelling up the call stack.
Throwing an exception is the right way to signal that a method received invalid input or that a precondition was not met. It is far better than returning a sentinel value (like -1 or null) because the caller cannot accidentally ignore an exception the way it can ignore a return value.
throw Statement: Input Validation
JavaA createUser method that throws IllegalArgumentException rather than silently accepting invalid input.
throws Keyword
The throws keyword appears in a method signature and declares that the method may throw one or more checked exceptions. It is a contract with the caller: "I might not complete normally, and here is the type of problem I might signal." The caller must then either catch those exceptions or propagate them further by adding its own throws clause.
You can technically use throws with unchecked exceptions too, but it is not required by the compiler and adds noise without enforcement. It is occasionally done as documentation when the unchecked exception is a meaningful part of the method's contract.
throws in a Method Signature
JavaA file-reading method that declares the checked IOException it may propagate to its caller.
Common Built-in Exceptions
Java ships with a rich set of exception types. The ones below are the ones you will encounter most often in everyday Java code. Recognising them on sight makes debugging significantly faster.
NullPointerException
Thrown when you try to use a null reference as though it were an object.
- Calling a method on a null reference: null.length()
- Accessing a field on a null reference: null.value
- Using null as an array: null[0]
- Java 14+ Helpful NPE messages tell you exactly which variable was null in the stack trace.
ArrayIndexOutOfBoundsException
Thrown when you try to access an array element with an index that is less than 0 or greater than or equal to the array length.
- int[] arr = {1, 2, 3}; arr[3] throws this exception because valid indices are 0, 1, and 2.
- It is unchecked, so the compiler will not warn you. Always validate indices or prefer the enhanced for-each loop and collections that do bounds checking internally.
ClassCastException
Thrown when you try to cast an object to a type it is not an instance of.
- Object obj = "hello"; Integer i = (Integer) obj; throws ClassCastException at runtime.
- Use instanceof before casting to avoid it: if (obj instanceof Integer i) { ... }
NumberFormatException
Thrown by parsing methods when the input string does not represent a valid number.
- Integer.parseInt("abc") throws this exception.
- It extends IllegalArgumentException, which extends RuntimeException, so it is unchecked.
- Always validate user input before parsing, or wrap the parse call in a try-catch.
ArithmeticException
Thrown for illegal arithmetic operations. In Java, the most common cause is integer division by zero.
- int result = 10 / 0 throws ArithmeticException: / by zero.
- Floating-point division by zero does NOT throw an exception; it returns Infinity or NaN instead.
StackOverflowError
Thrown by the JVM when the call stack exceeds its maximum depth, almost always due to infinite recursion.
- A method that calls itself without a base case will keep adding frames to the stack until the JVM runs out of stack space.
- It extends Error, not Exception. You should not catch it in normal code; fix the recursion bug instead.
OutOfMemoryError
Thrown when the JVM cannot allocate memory for a new object because the heap is exhausted.
- Common causes: creating very large arrays, memory leaks that prevent GC from reclaiming objects, or loading an enormous file entirely into memory.
- It extends Error. The correct response is to fix the memory usage, not to catch the error.
IllegalArgumentException
Thrown to signal that a method has received an argument that is inappropriate in value.
- You throw this yourself when a caller passes a value your method cannot accept: a negative length, a null key, or a string that is too short.
- It is unchecked and extends RuntimeException. It is the idiomatic way to enforce preconditions on method arguments.
IllegalStateException
Thrown when a method is called at a time when the object is not in an appropriate state to handle it.
- Calling iterator.next() after hasNext() returns false throws this.
- Calling a method on a connection that has already been closed is another classic case.
- The distinction from IllegalArgumentException is that the problem is not the argument value but the state of the object receiving the call.
Common Exceptions in Action
JavaTriggering and catching the most common exceptions to see their messages and types.
Multi-catch Block (Java 7+)
Before Java 7, if you wanted to handle two unrelated exception types with the same code, you had two options: write duplicate catch blocks or catch their nearest common ancestor (usually the too-broad Exception). Java 7 introduced the multi-catch block to eliminate this awkwardness.
Separate the exception types with a pipe character (|) inside one catch clause. The caught variable is implicitly final in a multi-catch block, which means you cannot reassign it inside the handler. You also cannot list exception types that are in a parent-child relationship in the same multi-catch, because the child would be redundant.
Multi-catch Block (Java 7+)
JavaHandling NumberFormatException and ArrayIndexOutOfBoundsException with a single handler.
Custom Exception Classes
The built-in exceptions cover a lot of ground, but there are times when none of them accurately describes a domain-specific problem in your application. Creating a custom exception class gives you a named type that callers can catch precisely, and it lets you carry additional context beyond a plain string message.
The convention is straightforward: extend Exception if the condition is something callers should be required to handle (a checked exception), or extend RuntimeException if it represents a programming error or a condition the caller cannot reasonably recover from (an unchecked exception). Always provide at least two constructors: one that takes a message string, and one that takes a message string plus a cause (the wrapped original exception).
Custom Exception Classes
JavaA domain-specific InsufficientFundsException carrying extra context beyond a plain message.
Exception Chaining (initCause and getCause)
Exception chaining is the practice of catching a low-level exception and wrapping it inside a higher-level exception before re-throwing. This preserves the original cause rather than losing it, while also allowing you to present a more meaningful error type to the caller.
The most common way to chain exceptions is to pass the original exception as the second argument to the new exception's constructor. All standard exception constructors accept a Throwable cause parameter. To retrieve the wrapped cause later, call getCause() on the caught exception. initCause() is an alternative when you cannot pass the cause to the constructor directly, though this situation is rare with modern exception classes.
Why exception chaining matters
The alternative to chaining is throwing only the high-level exception, which destroys information.
- Without chaining: a caller that catches ServiceException sees only "Failed to load user profile" and has no idea whether the root cause was a network timeout, a database failure, or a file not found. Debugging requires log files.
- With chaining: getCause() reveals the original SQLException or IOException, giving the full picture in a single stack trace printed by printStackTrace().
- The JVM prints all chained causes when you call e.printStackTrace(), showing each "Caused by:" level in the chain.
Exception Chaining with getCause
JavaA three-layer call stack wrapping a raw NumberFormatException inside domain-specific exceptions.
Quiz - Test Your Knowledge
Ten questions covering the exception hierarchy, checked and unchecked exceptions, try-catch-finally, try-with-resources, throw and throws, common built-in exceptions, custom exceptions, exception chaining, and multi-catch blocks. Read each option carefully before selecting your answer.
Knowledge Check
1. What is the root class of Java's entire exception hierarchy?
2. Which of the following is a checked exception?
3. When multiple catch blocks are chained, what rule governs their order?
4. What is the purpose of the finally block?
5. What does try-with-resources guarantee that a plain try-finally does not?
6. What is the difference between throw and throws in Java?
7. Which statement about NullPointerException is correct?
8. What is exception chaining?
9. How does a multi-catch block (Java 7+) differ from separate catch blocks?
10. When creating a custom exception class, what is the conventional rule for deciding whether it should extend Exception or RuntimeException?