Exception Handling in Java
Exception Handling in Java
No matter how carefully you write a program, things go wrong at runtime: a file is missing, a user types text where a number was expected, a network call times out. Exception handling is Java's structured mechanism for dealing with these situations gracefully, instead of letting the entire program crash without warning.
What is Exception Handling?
An exception is an event that disrupts the normal flow of a program's execution. When something goes wrong, Java creates an exception object describing what happened, and hands control up the call stack, searching for code that knows how to deal with that specific problem.
Exception handling lets you separate your normal, happy-path logic from your error-handling logic, rather than littering every line with manual error checks. It also lets a problem detected deep inside a method be handled much further up the call chain, wherever it makes the most sense to actually deal with it.
Types of Exceptions
Java organises all error conditions under a single root class, Throwable, which splits into two main branches.
Category | Examples | Typically Represents |
Exception | IOException, SQLException, NullPointerException | Problems a well-written program might reasonably anticipate or recover from |
Error | OutOfMemoryError, StackOverflowError | Serious problems with the runtime environment itself, rarely something application code should try to catch |
Within Exception, Java further distinguishes checked exceptions from unchecked (runtime) exceptions, a distinction important enough to get its own section next.
Checked vs Unchecked Exceptions
A checked exception represents a condition the compiler forces you to acknowledge; you must either catch it or explicitly declare that your method might throw it. These typically model problems outside the program's direct control, such as a missing file or a failed network connection.
An unchecked exception (a subclass of RuntimeException) is not checked by the compiler at all; you are never forced to catch or declare it. These typically signal a bug in the program's own logic, like dividing by zero or calling a method on a null reference.
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
// FileNotFoundException is checked — the compiler forces this 'throws' declaration
FileReader reader = new FileReader(new File("data.txt"));
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // ArrayIndexOutOfBoundsException — unchecked, no declaration needed
}
}
Aspect | Checked Exception | Unchecked Exception |
Checked by compiler? | Yes — must catch or declare with throws | No — handling is entirely optional |
Superclass | Exception (excluding RuntimeException) | RuntimeException |
Typically represents | A recoverable, expected problem outside the program's control | A programming bug or misuse of an API |
Common examples | IOException, SQLException, ParseException | NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException |
try, catch, and finally Blocks
The try block wraps code that might throw an exception. One or more catch blocks follow, each ready to handle a specific exception type. An optional finally block runs no matter what happens, whether the try block succeeded, failed, or even returned early, making it the natural place for cleanup code.
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Caught: " + e.getMessage());
} catch (Exception e) {
System.out.println("Caught a more general exception");
} finally {
System.out.println("This always runs, error or not");
}
System.out.println("Program continues normally");
}
}
When multiple catch blocks are present, Java checks them top to bottom and runs the first one whose exception type matches (or is a superclass of) the thrown exception, so more specific exception types should always be listed before more general ones.
Since Java 7, a try-with-resources block automatically closes resources like files or database connections for you, calling their close() method when the block ends, even if an exception occurs, which removes the need for a manual finally block dedicated purely to cleanup.
try (FileReader reader = new FileReader("data.txt")) {
// use reader here
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
} // reader.close() is called automatically here, even if an exception was thrown
throw and throws Keywords
throw is used inside a method body to actually raise an exception object at the moment something goes wrong. throws is used in a method's signature to declare that calling this method might result in a given checked exception being raised, so callers know they need to handle it.
public class Main {
static void withdraw(double balance, double amount) throws IllegalArgumentException {
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds");
}
System.out.println("Withdrawal successful");
}
public static void main(String[] args) {
try {
withdraw(100, 500);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Note the subtle difference in spelling and role: throw is an action, performed once, on a specific exception object. throws is a declaration, part of a method's signature, describing what kinds of checked exceptions might come out of it.
Custom Exceptions
Java lets you define your own exception types by extending Exception (to create a new checked exception) or RuntimeException (to create a new unchecked exception). This is valuable when a generic exception type doesn't capture the specific, meaningful error condition in your own business logic.
// A custom checked exception
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
class BankAccount {
private double balance = 100;
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Cannot withdraw " + amount + ", balance is " + balance);
}
balance -= amount;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
try {
account.withdraw(500);
} catch (InsufficientFundsException e) {
System.out.println("Transaction failed: " + e.getMessage());
}
}
}
Oracle's own guidance is the rule of thumb most teams follow: make an exception checked if the caller can reasonably be expected to recover from it, and unchecked if the problem really reflects a bug that no amount of catching will meaningfully fix.
Best Practices for Exception Handling
• Catch specific exception types rather than a blanket catch (Exception e), so you handle each kind of failure appropriately instead of masking unrelated bugs.
• Never leave a catch block completely empty; at minimum, log the error, since a silently swallowed exception is one of the hardest bugs to track down later.
• Don't use exceptions for ordinary control flow, like exiting a loop early; reserve them for genuinely exceptional, unexpected situations.
• Include a clear, specific message when throwing an exception, describing exactly what went wrong, since this message is often the only clue available when debugging a production issue.
• Close resources properly using try-with-resources rather than manual finally blocks wherever the resource supports it.
Did You Know?
Many modern frameworks, including widely-used ones like Spring, deliberately favour unchecked exceptions throughout their own APIs, even converting checked exceptions from lower-level libraries into unchecked ones before they reach your code. This reflects a broader industry shift towards using checked exceptions sparingly, since they can make method signatures verbose without always providing a meaningful recovery path for the caller.










