Java Exception Handling
1) What is an Exception in Java
In Java, an exception is an event that occurs during the execution of a program that disrupts the normal flow of instructions. Unlike an ordinary error code, an exception is represented as an object that contains details about the problem, including its type, message, and the point in the program where it occurred.
All exceptions in Java are derived from the Throwable class, which is part of java.lang. The two main direct subclasses of Throwable are Error and Exception. Errors represent serious problems that a program should not attempt to handle (such as OutOfMemoryError), whereas exceptions represent conditions that an application might be able to handle.
When an exception occurs, the normal flow of execution is halted, and the runtime system begins a search for an appropriate block of code to handle that exception. If no such code is found, the program terminates abruptly.
Example:
public class ExceptionIntro {
public static void main(String[] args) {
System.out.println("Before exception");
int result = 10 / 0; // Division by zero causes ArithmeticException
System.out.println("After exception"); // This line is never reached
}
}
Output:
Before exception
Exception in thread "main" java.lang.ArithmeticException: / by zero
at ExceptionIntro.main(ExceptionIntro.java:5)
In this example, the statement 10 / 0 results in an ArithmeticException. The JVM stops normal execution at that point. Since no exception handler is provided, the default exception handler prints a stack trace showing the type of exception, the reason (/ by zero), and the exact location where it occurred.
2) Why Exception Handling is Needed
Without exception handling, the moment an unexpected event occurs, the program terminates. This may be acceptable in trivial scripts, but in any serious application, abrupt termination is undesirable because it leaves operations incomplete, resources unreleased, and users without useful feedback.
Exception handling provides a structured way to detect problems, isolate them from normal logic, and either recover from them or fail gracefully. By using try-catch blocks, a program can:
- Prevent complete failure by handling only the parts that are affected.
- Display meaningful messages to users instead of cryptic stack traces.
- Log detailed diagnostic information for developers.
- Release resources such as files, network connections, or database handles in all cases.
Consider a program that reads a configuration value for a network port:
public class WhyHandling {
static int parsePort(String s) {
return Integer.parseInt(s); // May throw NumberFormatException
}
public static void main(String[] args) {
int port;
try {
port = parsePort("eighty");
} catch (NumberFormatException e) {
System.err.println("Invalid port; using default 8080. Details: " + e.getMessage());
port = 8080; // Fallback value
}
System.out.println("Starting server on port " + port);
}
}
Output:
Invalid port; using default 8080. Details: For input string: "eighty"
Starting server on port 8080
Here, the invalid string "eighty" cannot be converted to an integer, and Integer.parseInt throws a NumberFormatException. Instead of allowing this to crash the application, the exception is caught, a clear message is printed, and a default port value is used. The program continues running normally.
Alright, continuing in the same full-fledged, textbook style — here’s Section 3.
3) Throwable Hierarchy and Types of Exceptions
Every exception in Java is represented by an object that descends from the Throwable class in java.lang. The Throwable class has two direct subclasses:
- Error – Represents serious problems that a reasonable application should not try to catch. Errors usually indicate system-level issues such as hardware failure or JVM-related problems. Examples include OutOfMemoryError, StackOverflowError, and NoClassDefFoundError. Since these are beyond the control of the application, they are unchecked and generally not handled programmatically.
- Exception – Represents conditions that an application might want to catch and handle. The Exception class is further divided into:
- Checked exceptions – Exceptions that the compiler forces you to either handle using try-catch or declare using the throws keyword. They usually represent recoverable conditions, such as file not found, network timeout, or invalid user input in a way that the program can anticipate. Examples: IOException, SQLException, ParseException.
- Unchecked exceptions – These are subclasses of RuntimeException and represent programming errors, logic mistakes, or improper API usage. The compiler does not require them to be caught or declared. Examples: NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException.
Visual hierarchy:
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error (Unchecked, do not catch normally)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── java.lang.Exception
├── Checked exceptions (must handle or declare)
│ ├── IOException
│ └── SQLException
└── Unchecked exceptions (RuntimeException and subclasses)
├── NullPointerException
├── ArithmeticException
└── IllegalArgumentException
Example: Checked vs Unchecked Exceptions
import java.io.*;
public class HierarchyDemo {
static void checkedExample() throws IOException {
// FileReader constructor throws FileNotFoundException (checked exception)
FileReader reader = new FileReader("missing.txt");
reader.close();
}
static void uncheckedExample() {
String text = null;
// Causes NullPointerException (unchecked exception)
System.out.println(text.length());
}
public static void main(String[] args) {
// Checked exception handling
try {
checkedExample();
} catch (IOException e) {
System.out.println("Caught checked exception: " + e.getClass().getSimpleName());
}
// Unchecked exception handling
try {
uncheckedExample();
} catch (RuntimeException e) {
System.out.println("Caught unchecked exception: " + e.getClass().getSimpleName());
}
}
}
Output:
Caught checked exception: FileNotFoundException
Caught unchecked exception: NullPointerException
Explanation:
- Checked example:
FileReader tries to open a file named missing.txt. Since the file does not exist, it throws a FileNotFoundException, which is a checked exception. The compiler forces you to either handle it with a try-catch block or declare it with throws in the method signature. In this case, the method checkedExample() declares throws IOException, and the caller handles it. - Unchecked example:
Calling .length() on a null reference causes a NullPointerException, which is a subclass of RuntimeException. The compiler does not require any explicit handling for this. The program can still catch it at runtime if desired, but often these exceptions indicate programming errors that should be fixed rather than caught.
4) How Exceptions Work in Java
When something goes wrong during program execution, Java uses its built-in exception-handling mechanism to transfer control from the point of error to an appropriate handler. Understanding the exact steps involved is crucial for writing reliable code.
4.1 Throwing an Exception
An exception can be thrown in two ways:
- By the Java runtime – for example, division by zero causes the JVM to create and throw an ArithmeticException.
- Manually by the programmer – using the throw statement with an exception object.
When an exception is thrown:
- The JVM creates an object of the exception class.
- The current execution is halted immediately at that point.
- Control is transferred to the nearest matching catch block up the call stack.
4.2 Propagation
If the current method does not handle the exception, it is propagated back to the caller method. This process continues up the call stack until:
- A matching catch block is found, or
- The exception reaches the main method without being caught, in which case the JVM’s default handler prints the stack trace and terminates the program.
4.3 Stack Unwinding
When an exception propagates, the JVM unwinds the call stack, which means:
- The method where the exception occurred is exited immediately.
- All methods higher in the call chain are exited until a suitable handler is found.
- As methods are exited, their local variables go out of scope and objects eligible for garbage collection can be freed.
Example: Propagation in Action
public class PropagationDemo {
static void level3() {
System.out.println("Level 3: about to divide by zero");
int x = 5 / 0; // ArithmeticException
System.out.println("Level 3: end"); // never reached
}
static void level2() {
System.out.println("Level 2: calling level3");
level3();
System.out.println("Level 2: end"); // never reached if exception not handled
}
static void level1() {
System.out.println("Level 1: calling level2");
try {
level2();
} catch (ArithmeticException e) {
System.out.println("Caught exception in level1: " + e);
}
System.out.println("Level 1: end");
}
public static void main(String[] args) {
level1();
System.out.println("Main: end");
}
}
Output:
Level 1: calling level2
Level 2: calling level3
Level 3: about to divide by zero
Caught exception in level1: java.lang.ArithmeticException: / by zero
Level 1: end
Main: end
Explanation:
- The call starts in main(), which calls level1().
- level1() calls level2(), which in turn calls level3().
- level3() performs 5 / 0, triggering an ArithmeticException.
- Since level3() does not handle it, the exception propagates back to level2() (stack unwinding). level2() also doesn’t handle it, so it propagates further to level1().
- level1() has a try-catch block that catches the ArithmeticException.
- After catching, execution resumes after the catch block, and the program continues without abrupt termination.
This sequence shows how exceptions travel back through the call stack and why placement of catch blocks determines where recovery happens.
5) try–catch: syntax, matching rules, ordering, and multi-catch
try–catch surrounds a block of code that may fail. If any statement inside the try throws an exception, the normal flow stops immediately and control jumps to the first catch whose parameter type matches the thrown exception (using normal inheritance rules). After a matching catch finishes, execution continues after the entire try–catch construct.
5.1 Basic form and flow
// File: TryCatchBasic.java
public class TryCatchBasic {
public static void main(String[] args) {
System.out.println("A");
try {
System.out.println("B");
int x = 10 / 0; // throws ArithmeticException
System.out.println("C"); // skipped
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
System.out.println("D");
}
}
Output:
A
B
Caught: ArithmeticException
D
Explanation: As soon as 10 / 0 throws, control transfers to the catch. The line System.out.println("C") is never executed.
5.2 If the type doesn’t match, it isn’t caught
// File: TryCatchMismatch.java
public class TryCatchMismatch {
public static void main(String[] args) {
try {
String s = null;
s.length(); // NullPointerException
} catch (ArithmeticException e) {
System.out.println("Will not run");
}
System.out.println("This line is not reached because the program aborts.");
}
}
Expected behavior:
- There is no matching catch for NullPointerException, so the exception remains uncaught and the program terminates with a stack trace after leaving the try block. The final println is never executed.
5.3 Ordering: specific before general (first match wins)
Put more specific exceptions earlier and the more general ones later. The compiler will reject unreachable broader catches placed before narrower ones.
// File: CatchOrder.java
public class CatchOrder {
public static void main(String[] args) {
try {
Object[] a = new String[2];
a[0] = 123; // ArrayStoreException (a RuntimeException)
} catch (ArrayStoreException e) {
System.out.println("Caught specific: " + e.getClass().getSimpleName());
} catch (RuntimeException e) {
System.out.println("Caught general runtime");
}
}
}
Output:
Caught specific: ArrayStoreException
If you reverse the order:
// (Don’t compile this)
// catch (RuntimeException e) { ... }
// catch (ArrayStoreException e) { ... } // Unreachable; compiler error
The second catch is unreachable because RuntimeException already matches ArrayStoreException. The compiler reports an error.
5.4 Catching by supertype vs catching specifically
Catching Exception or RuntimeException will handle many cases but can hide real causes and make recovery logic too generic. Prefer catching specific types when you can do something meaningful.
// File: CatchSupertype.java
import java.io.*;
public class CatchSupertype {
static void riskyIO() throws IOException {
new FileReader("missing.txt"); // FileNotFoundException (checked)
}
public static void main(String[] args) {
try {
riskyIO();
} catch (FileNotFoundException e) { // specific
System.out.println("Missing file: " + e.getMessage());
} catch (IOException e) { // supertype, other IO issues
System.out.println("Other IO issue: " + e.getClass().getSimpleName());
}
}
}
Possible output:
Missing file: missing.txt (The system cannot find the file specified)
5.5 Multi-catch (|) for sibling types
Use multi-catch to handle several independent exception types with the same handling logic. The caught variable is implicitly final (you cannot reassign it).
// File: MultiCatch.java
import java.io.*;
public class MultiCatch {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("missing.txt"))) {
System.out.println(br.readLine());
} catch (FileNotFoundException | SecurityException e) {
// e = new RuntimeException(); // compile error: effectively final
System.out.println("Access or existence problem: " + e.getClass().getSimpleName());
} catch (IOException e) {
System.out.println("Other IO issue: " + e.getClass().getSimpleName());
}
}
}
Output (typical):
Access or existence problem: FileNotFoundException
Rules and notes:
- Types combined with | must be disjoint in inheritance (neither is a subtype of the other), otherwise one would make the other unreachable.
- The first matching catch is selected; later catch blocks are ignored.
5.6 Scope and variable lifetime around try–catch
Variables declared inside the try are not visible outside it. If you need to use a computed value whether or not an exception happens, declare it before the try.
// File: TryScope.java
public class TryScope {
public static void main(String[] args) {
String value; // declared outside to use after try
try {
value = compute(); // may throw
} catch (RuntimeException e) {
value = "fallback";
}
System.out.println("Result = " + value);
}
static String compute() {
if (System.nanoTime() % 2 == 0) throw new IllegalStateException("random");
return "ok";
}
}
Possible outputs:
Result = ok
or
Result = fallback
5.7 Don’t swallow exceptions silently
An empty catch hides failures and makes issues hard to diagnose.
// File: SwallowingBad.java
public class SwallowingBad {
public static void main(String[] args) {
try {
int x = 10 / 0;
} catch (ArithmeticException e) {
// Bad: nothing here. At least log or rethrow with context.
}
System.out.println("Continues, but root cause is hidden.");
}
}
A better pattern is to log meaningful context or translate and rethrow:
// File: HandleOrTranslate.java
public class HandleOrTranslate {
public static void main(String[] args) {
try {
int x = 10 / 0;
} catch (ArithmeticException e) {
System.err.println("Computation failed: divide by zero. Inputs: a=10, b=0");
// Optionally: throw new IllegalStateException("Calculation failed", e);
}
}
}
5.8 Preserving vs changing the exception
Catching and rethrowing the same exception preserves the original stack trace:
// File: RethrowSame.java
public class RethrowSame {
static void f() {
try {
throw new IllegalArgumentException("bad");
} catch (IllegalArgumentException e) {
// Preserve: rethrow same object
throw e;
}
}
public static void main(String[] args) { f(); }
}
If you throw a new exception, consider passing the original as the cause so diagnostics aren’t lost:
// File: WrapWithCause.java
public class WrapWithCause {
static void f() {
try {
throw new IllegalArgumentException("bad");
} catch (IllegalArgumentException e) {
throw new RuntimeException("Higher-level context: validating user input", e);
}
}
public static void main(String[] args) { f(); }
}
The second example produces a stack trace headed by RuntimeException, but getCause() reveals the original IllegalArgumentException.
5.9 Matching is based on inheritance (instanceof rules)
A catch (IOException) will match FileNotFoundException, because the latter is a subclass of IOException. A catch (Exception) matches nearly all exceptions except Error. A catch (Throwable) matches everything, including Error, but should be used rarely and carefully.
// File: InheritanceMatch.java
import java.io.*;
public class InheritanceMatch {
public static void main(String[] args) {
try {
new FileReader("nope.txt");
} catch (FileNotFoundException e) { // first: most specific
System.out.println("Specific: " + e.getClass().getSimpleName());
} catch (IOException e) { // then the supertype
System.out.println("General IO: " + e.getClass().getSimpleName());
} catch (Exception e) { // very general
System.out.println("Very general: " + e.getClass().getSimpleName());
}
}
}
Output:
Specific: FileNotFoundException
6) The finally Block — Guarantees, Uses, and Pitfalls
The finally block is an optional part of a try–catch statement.
It is always executed after the try block finishes, regardless of whether an exception was thrown or caught, except in very rare JVM-termination scenarios.
6.1 Basic syntax
try {
// code that may throw
} catch (SomeException e) {
// handling code
} finally {
// cleanup code
}
- You can have try + finally without a catch.
- You can have multiple catch blocks, but only one finally.
6.2 Purpose: resource cleanup
finally is designed for cleanup actions that must run regardless of whether the try succeeded or failed.
Typical examples:
- Closing files or sockets
- Releasing locks
- Restoring modified system properties
- Disconnecting from databases
6.3 Always executed (with rare exceptions)
// File: FinallyAlways.java
public class FinallyAlways {
public static void main(String[] args) {
try {
System.out.println("Inside try");
int x = 10 / 0; // throws ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Caught exception");
} finally {
System.out.println("Finally block runs");
}
System.out.println("Program continues...");
}
}
Output:
Inside try
Caught exception
Finally block runs
Program continues...
Even if no exception occurs, finally still runs:
// File: FinallyNoException.java
public class FinallyNoException {
public static void main(String[] args) {
try {
System.out.println("Inside try");
} finally {
System.out.println("Finally runs even without catch");
}
System.out.println("Program continues...");
}
}
Output:
Inside try
Finally runs even without catch
Program continues...
6.4 Finally without catch
You can use try + finally without catch when you want cleanup but don’t want to handle the exception here.
// File: TryFinallyOnly.java
public class TryFinallyOnly {
public static void main(String[] args) {
try {
System.out.println("Trying risky work");
int x = 5 / 0; // unhandled exception
} finally {
System.out.println("Cleanup in finally");
}
System.out.println("Never reached");
}
}
Output:
Trying risky work
Cleanup in finally
Exception in thread "main" java.lang.ArithmeticException: / by zero
at TryFinallyOnly.main(TryFinallyOnly.java:6)
Note: The exception is still thrown after the finally block executes.
6.5 The rare cases where finally doesn’t execute
The finally block is skipped only if:
- The JVM shuts down abruptly (System.exit(), fatal error, power loss).
- The thread is killed in a way that prevents further execution.
Example:
// File: FinallySkip.java
public class FinallySkip {
public static void main(String[] args) {
try {
System.out.println("Before exit");
System.exit(0); // forces JVM termination
} finally {
System.out.println("This will NOT run");
}
}
}
Output:
Before exit
6.6 Interaction with return and exceptions
finally still executes even if return is called inside try or catch.
// File: FinallyWithReturn.java
public class FinallyWithReturn {
static int compute() {
try {
System.out.println("Inside try");
return 1;
} finally {
System.out.println("Finally runs before method return");
}
}
public static void main(String[] args) {
System.out.println("Result: " + compute());
}
}
Output:
Inside try
Finally runs before method return
Result: 1
6.7 Finally overriding a return value (pitfall)
If you return from both try and finally, the return in finally overrides the one from try.
Avoid this, as it makes code confusing.
// File: FinallyReturnOverride.java
public class FinallyReturnOverride {
static int test() {
try {
return 1;
} finally {
return 2; // overrides previous return
}
}
public static void main(String[] args) {
System.out.println(test());
}
}
Output:
2
6.8 Exception in finally overrides original exception (dangerous)
If finally throws an exception, it replaces any exception from the try or catch, potentially hiding the real problem.
// File: FinallyExceptionOverride.java
public class FinallyExceptionOverride {
static void process() {
try {
throw new RuntimeException("Try exception");
} finally {
throw new RuntimeException("Finally exception"); // hides original
}
}
public static void main(String[] args) {
process();
}
}
Output:
Exception in thread "main" java.lang.RuntimeException: Finally exception
at FinallyExceptionOverride.process(FinallyExceptionOverride.java:6)
...
Here, the "Try exception" is completely lost.
Best practice: Avoid throwing new exceptions in finally unless you deliberately want to override.
6.9 Finally for manual resource management
Before Java 7, finally was the standard way to ensure closing of resources.
// File: FinallyClose.java
import java.io.*;
public class FinallyClose {
public static void main(String[] args) throws IOException {
FileReader fr = null;
try {
fr = new FileReader("data.txt");
System.out.println((char) fr.read());
} finally {
if (fr != null) {
fr.close();
System.out.println("File closed");
}
}
}
}
6.10 Prefer try-with-resources in modern Java
From Java 7 onward, try-with-resources automatically closes resources that implement AutoCloseable, reducing the need for finally in cleanup scenarios.
// File: TryWithResources.java
import java.io.*;
public class TryWithResources {
public static void main(String[] args) throws IOException {
try (FileReader fr = new FileReader("data.txt")) {
System.out.println((char) fr.read());
}
}
}
This ensures fr.close() is called automatically, even if an exception occurs.
7) Throwing Exceptions — throw Keyword and Rules
In Java, exceptions can be thrown either by:
- The Java runtime (automatically, when an error occurs).
- The programmer (manually, using the throw statement).
This section focuses on manual throwing with the throw keyword.
7.1 Syntax of throw
throw new ExceptionType("Error message");
- throw is a statement (not a method) used to pass an exception object to the runtime.
- The object must be an instance of Throwable (i.e., Exception or Error or their subclasses).
- After throw, no code in the current block executes — control immediately leaves that block and starts the propagation process.
7.2 Example — Throwing an exception manually
// File: ThrowDemo.java
public class ThrowDemo {
public static void main(String[] args) {
try {
checkAge(15);
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage());
}
System.out.println("Program continues...");
}
static void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("Age must be at least 18");
}
System.out.println("Access granted");
}
}
Output:
Caught: Age must be at least 18
Program continues...
7.3 Rules for using throw
- You can throw only one exception at a time using a single throw statement.
- The object must be a Throwable — i.e., it must be Exception, Error, or any subclass.
- Checked exceptions must either be handled (try–catch) or declared with throws in the method signature.
- Unchecked exceptions (RuntimeException and subclasses) can be thrown without being declared in throws.
7.4 Throwing checked vs. unchecked exceptions
Checked exception example
import java.io.IOException;
public class ThrowChecked {
static void risky() throws IOException {
throw new IOException("I/O failed");
}
public static void main(String[] args) {
try {
risky();
} catch (IOException e) {
System.out.println("Caught: " + e);
}
}
}
Here, IOException is checked, so risky() must declare throws IOException.
Unchecked exception example
public class ThrowUnchecked {
static void risky() {
throw new ArithmeticException("Division by zero");
}
public static void main(String[] args) {
risky(); // no throws declaration needed
}
}
ArithmeticException is unchecked, so no throws clause is required.
7.5 Difference between throw and throws
Aspect | throw | throws |
---|---|---|
Usage | To actually throw an exception object at runtime. | To declare exceptions a method may throw. |
Position | Inside method body. | In method declaration. |
Number of Exceptions | Can throw only one exception at a time. | Can declare multiple exceptions separated by commas. |
Example | throw new IOException(); | void read() throws IOException, SQLException |
7.6 Throwing exceptions in constructors
Constructors can also throw exceptions, either checked or unchecked.
import java.io.IOException;
public class ThrowInConstructor {
ThrowInConstructor() throws IOException {
throw new IOException("Constructor failed");
}
public static void main(String[] args) {
try {
new ThrowInConstructor();
} catch (IOException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
7.7 Throwing custom exceptions
You can throw your own user-defined exception class.
class InvalidBalanceException extends Exception {
InvalidBalanceException(String msg) {
super(msg);
}
}
public class CustomThrow {
static void withdraw(double balance, double amount) throws InvalidBalanceException {
if (amount > balance) {
throw new InvalidBalanceException("Insufficient balance");
}
System.out.println("Withdrawal successful");
}
public static void main(String[] args) {
try {
withdraw(500, 700);
} catch (InvalidBalanceException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
7.8 Best practices when throwing exceptions
- Use meaningful messages: Helps debugging.
- Throw specific exceptions rather than general Exception.
- Avoid overusing unchecked exceptions for normal control flow.
- Don’t lose original exceptions — wrap them using exception chaining if needed.
Example of exception chaining:
try {
readFile();
} catch (IOException e) {
throw new RuntimeException("Failed to read configuration", e);
}
Throw vs Throws (Checked Exceptions & Signatures)
1. What is throw?
- Purpose: Used to actually throw an exception object at runtime.
- Location: Inside the method body or constructor.
- Quantity: Can throw only one exception instance per throw statement.
- Runtime Behavior: Once executed, control immediately jumps out of the current block, and normal execution stops until the exception is caught or propagates to JVM.
Syntax:
throw new ExceptionType("Message");
Example with throw:
public class ThrowExample {
static void check(int age) {
if (age < 18) {
throw new IllegalArgumentException("Underage not allowed");
}
System.out.println("Welcome!");
}
public static void main(String[] args) {
try {
check(15);
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}
Output:
Caught: Underage not allowed
2. What is throws?
- Purpose: Used in a method signature to declare the exceptions a method can throw.
- Effect: Informs the caller that the method may throw certain checked exceptions, so they must either handle them or declare them.
- Quantity: Can declare multiple exceptions separated by commas.
- Use Case: Required for checked exceptions unless handled inside.
Syntax:
returnType methodName(params) throws ExceptionType1, ExceptionType2
Example with throws:
import java.io.*;
public class ThrowsExample {
static void readFile() throws IOException {
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
System.out.println(br.readLine());
br.close();
}
public static void main(String[] args) {
try {
readFile();
} catch (IOException e) {
System.out.println("Caught: " + e);
}
}
}
3. Checked vs Unchecked in throw/throws
- Checked exceptions:
- Must be either caught (try–catch) or declared using throws.
- Examples: IOException, SQLException.
- Unchecked exceptions (RuntimeException subclasses):
- No requirement to declare in throws.
- Examples: NullPointerException, ArithmeticException.
Example:
// Checked
void risky() throws IOException { throw new IOException(); }
// Unchecked
void risky() { throw new ArithmeticException(); }
4. Key Differences Table
Feature | throw | throws |
---|---|---|
Purpose | Actually throws exception at runtime | Declares possible exceptions in method signature |
Location | Inside method body | After method parameters in signature |
Exceptions | One at a time | Multiple allowed |
Compile-time Requirement | No need to declare for unchecked | Must declare for checked if not handled |
Example | throw new IOException(); | void m() throws IOException |
5. Common Mistakes
- Declaring in throws but not actually throwing anything — not harmful but confusing.
- Throwing checked exception without throws in signature — compile-time error.
- Thinking throws will throw exceptions automatically — it only declares, not executes.
Java Exception Handling – Advanced Topics
1. Propagation, Rethrowing, and Wrapping (Precise Rethrow)
Exception Propagation
When an exception is thrown inside a method and not caught there, it propagates back through the call stack until:
- It is caught by a try-catch block.
- Or it reaches the JVM, which terminates the program.
Example:
public class PropagationExample {
static void level3() {
int x = 5 / 0; // ArithmeticException
}
static void level2() {
level3();
}
static void level1() {
level2();
}
public static void main(String[] args) {
try {
level1();
} catch (ArithmeticException e) {
System.out.println("Caught at main: " + e);
}
}
}
Output:
Caught at main: java.lang.ArithmeticException: / by zero
Rethrowing Exceptions
Sometimes you catch an exception, log it, and then throw it again to be handled elsewhere.
public class RethrowExample {
static void risky() throws Exception {
try {
throw new Exception("Something went wrong");
} catch (Exception e) {
System.out.println("Logging: " + e.getMessage());
throw e; // rethrow
}
}
public static void main(String[] args) {
try {
risky();
} catch (Exception e) {
System.out.println("Handled in main: " + e.getMessage());
}
}
}
Wrapping Exceptions
Useful when you want to change exception type but preserve the original cause.
public class WrapExample {
static void read() {
try {
throw new java.io.IOException("Disk not found");
} catch (java.io.IOException e) {
throw new RuntimeException("Failed to read file", e);
}
}
public static void main(String[] args) {
try {
read();
} catch (RuntimeException e) {
System.out.println(e.getMessage());
System.out.println("Cause: " + e.getCause());
}
}
}
2. Chained Exceptions (cause, initCause, getCause)
Java allows linking one exception to another so you know root cause.
Using Constructor:
public class ChainedExample {
public static void main(String[] args) {
try {
Throwable cause = new NumberFormatException("Invalid number");
throw new Exception("High-level exception", cause);
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("Cause: " + e.getCause());
}
}
}
Using initCause():
Exception e1 = new Exception("Wrapper");
e1.initCause(new NullPointerException("Original"));
throw e1;
3. Creating Custom Exceptions
Checked Exception Example:
class MyCheckedException extends Exception {
public MyCheckedException(String msg) {
super(msg);
}
}
public class CustomChecked {
public static void main(String[] args) {
try {
throw new MyCheckedException("Checked error occurred");
} catch (MyCheckedException e) {
e.printStackTrace();
}
}
}
Runtime Exception Example:
class MyRuntimeException extends RuntimeException {
public MyRuntimeException(String msg) {
super(msg);
}
}
4. Common Exceptions & How to Handle
- NullPointerException: Check for null before dereferencing.
- ArrayIndexOutOfBoundsException: Validate indices.
- ClassCastException: Use instanceof before casting.
- IOException: Always close streams in finally or try-with-resources.
5. Exception Handling with Method Overriding
- Overriding method cannot throw broader checked exceptions than the overridden method.
- Can throw narrower or unchecked exceptions.
6. final, finally, and finalize
- final: Keyword to make variable constant, method non-overridable, or class non-inheritable.
- finally: Block that always executes (except System.exit()).
- finalize(): Deprecated — called by GC before object removal.
7. Multi-catch & Effective Finality
try {
int a = 5 / 0;
} catch (ArithmeticException | NullPointerException e) {
System.out.println(e);
}
Variable e is implicitly final in multi-catch.
8. Stack Traces: printing, reading, and logging
try {
throw new Exception("Test");
} catch (Exception e) {
e.printStackTrace();
for (StackTraceElement ste : e.getStackTrace()) {
System.out.println(ste);
}
}
9. Performance Considerations
- Avoid exceptions for control flow.
- Stack trace generation is expensive — use selectively in performance-critical code.
10. Exceptions with Threads/Executors
Thread t = new Thread(() -> { throw new RuntimeException("Boom"); });
t.setUncaughtExceptionHandler((th, ex) -> System.out.println("Caught: " + ex));
t.start();
With CompletableFuture:
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Error"); })
.exceptionally(ex -> { System.out.println(ex); return null; });
11. Testing Exceptions with JUnit
import static org.junit.jupiter.api.Assertions.*;
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException();
});
}
12. Best Practices & Anti-patterns
- Catch specific exceptions.
- Avoid empty catch blocks.
- Don’t swallow exceptions silently.
- Wrap low-level exceptions into meaningful high-level ones.
13. FAQ / Troubleshooting
- InterruptedException: Always re-interrupt the thread.
- Throwable vs Exception: Don’t catch Throwable unless in top-level handlers.
14. End-to-End “Kitchen Sink” Example
public class KitchenSink {
static void validate(int age) throws Exception {
if (age < 0) throw new IllegalArgumentException("Negative age");
if (age < 18) throw new MyCheckedException("Underage");
}
public static void main(String[] args) {
try {
validate(15);
} catch (MyCheckedException e) {
System.out.println("Custom checked: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Illegal arg: " + e.getMessage());
}
}
}
15. Quick Reference / Cheat Sheet
- throw → actually throw exception
- throws → declare possible exception
- Checked → must handle/declare
- Unchecked → no requirement
- Multi-catch → types must be disjoint
- finally → always executes
- Avoid exceptions for normal logic
Next Blog- Complete Guide to Strings in Java: Creation, Memory, Methods & Best Practices