Why Exception Handling Matters
Imagine you're a pilot. Everything's going smoothly until suddenly an engine fails. What happens next determines whether everyone lands safely or not. A good pilot has procedures for every possible problem.
Exception handling is your "emergency procedures" for code. Things will go wrong - files won't exist, networks will fail, users will enter invalid data. Good exception handling ensures your application recovers gracefully instead of crashing.
// WITHOUT exception handling - program crashes
String input = null;
int length = input.length(); // NullPointerException - CRASH!
// WITH exception handling - program continues
String input = null;
try {
int length = input.length();
} catch (NullPointerException e) {
System.out.println("Input was null, using default");
int length = 0;
}
// Program continues normally
Prevent Crashes
Your application stays running even when errors occur. Users see friendly messages instead of scary error screens.
Find Bugs Faster
Good exception messages tell you exactly what went wrong and where, making debugging much easier.
Clean Up Resources
Properly close files, database connections, and network sockets even when errors occur.
Professional Code
Enterprise applications require robust error handling. It's expected in professional development.
Understanding Exception Types
Java has a hierarchy of exceptions. Understanding this helps you know which exceptions to catch and when.
Throwable (root class)
├── Error (serious problems - don't catch these)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
│
└── Exception (problems you should handle)
├── RuntimeException (unchecked - optional to catch)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── IllegalArgumentException
│ ├── NumberFormatException
│ └── ArithmeticException
│
└── Checked Exceptions (must catch or declare)
├── IOException
├── FileNotFoundException
├── SQLException
└── ClassNotFoundException
Checked vs Unchecked Exceptions
Checked Exceptions (Must Handle)
- Compiler forces you to handle them
- Usually external factors beyond your control
- Examples: File not found, network failure, database error
- Use when the caller can reasonably recover
Unchecked Exceptions (Optional to Handle)
- Extend RuntimeException
- Usually programming errors (bugs)
- Examples: Null pointer, array index, illegal argument
- Fix the code instead of catching these
Try-Catch: The Basics
The try-catch block is your primary tool for handling exceptions. Put risky code in "try" and handle problems in "catch".
try {
// Risky code that might throw an exception
int result = 10 / 0; // ArithmeticException!
} catch (ArithmeticException e) {
// Handle the exception
System.out.println("Cannot divide by zero!");
System.out.println("Error: " + e.getMessage());
}
// Program continues here
System.out.println("After exception handling");
Catching Multiple Exceptions
public void processFile(String filename) {
try {
// Multiple things can go wrong here
FileReader file = new FileReader(filename);
BufferedReader reader = new BufferedReader(file);
String line = reader.readLine();
int number = Integer.parseInt(line);
System.out.println("Number: " + number);
reader.close();
} catch (FileNotFoundException e) {
System.out.println("File not found: " + filename);
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
} catch (NumberFormatException e) {
System.out.println("File doesn't contain a valid number");
}
}
// Or catch multiple in one block (Java 7+)
try {
// risky code
} catch (FileNotFoundException | NumberFormatException e) {
System.out.println("Error: " + e.getMessage());
}
The Exception Object
try {
String text = null;
text.length();
} catch (NullPointerException e) {
// Useful methods on exception objects:
// Get error message
String message = e.getMessage(); // null
// Get full stack trace as string
e.printStackTrace(); // Prints where error occurred
// Get the class name of exception
String type = e.getClass().getName();
// Get cause (if this exception was caused by another)
Throwable cause = e.getCause();
}
Finally: Cleanup Code
The "finally" block runs NO MATTER WHAT - whether an exception occurred or not. It's perfect for cleanup code like closing files or connections.
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// Process file...
String data = readAllData(reader);
} catch (FileNotFoundException e) {
System.out.println("File not found");
} catch (IOException e) {
System.out.println("Error reading file");
} finally {
// This ALWAYS runs - even if exception occurred
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Error closing file");
}
}
System.out.println("Cleanup complete");
}
Try-With-Resources (Modern Way)
Java 7 introduced try-with-resources which automatically closes resources. This is the preferred approach - cleaner and safer.
// OLD WAY: Manual cleanup with finally
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// use reader...
} finally {
if (reader != null) {
reader.close();
}
}
// MODERN WAY: Try-with-resources (automatic cleanup!)
try (FileReader reader = new FileReader("data.txt")) {
// use reader...
} // reader automatically closed here!
// Multiple resources
try (
FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")
) {
// use both...
} // Both automatically closed!
// Works with any class implementing AutoCloseable
try (Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
// process results
}
} // All three closed automatically!
Throw and Throws
throw: Create an Exception
Use "throw" to create and throw an exception yourself. This is useful when you detect an error condition.
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
if (age > 150) {
throw new IllegalArgumentException("Age seems unrealistic: " + age);
}
this.age = age;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new IllegalStateException("Insufficient funds");
}
balance -= amount;
}
throws: Declare Exceptions
Use "throws" in a method signature to declare that this method might throw certain exceptions. The caller must handle them.
// This method might throw IOException - caller must handle it
public String readFile(String filename) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filename));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
reader.close();
return content.toString();
}
// Caller must handle the exception
public void processData() {
try {
String data = readFile("data.txt");
// process data...
} catch (IOException e) {
System.out.println("Failed to read file: " + e.getMessage());
}
}
// Or pass it up the chain
public void processData() throws IOException {
String data = readFile("data.txt"); // Exception passed to caller
}
throw vs throws
- throw - Actually throws an exception (action)
- throws - Declares that method might throw exception (declaration)
- throw is used inside method body
- throws is used in method signature
Custom Exceptions
Create your own exception classes when standard exceptions don't fit your needs. This makes your code more expressive and easier to debug.
// Simple custom exception
public class InsufficientFundsException extends Exception {
private double shortfall;
public InsufficientFundsException(String message) {
super(message);
}
public InsufficientFundsException(String message, double shortfall) {
super(message);
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
// Usage
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
double shortfall = amount - balance;
throw new InsufficientFundsException(
"Cannot withdraw $" + amount + ". Balance is only $" + balance,
shortfall
);
}
balance -= amount;
}
}
// Handling it
try {
account.withdraw(1000);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("You need $" + e.getShortfall() + " more");
}
Unchecked Custom Exception
// Extend RuntimeException for unchecked exception
public class UserNotFoundException extends RuntimeException {
private String userId;
public UserNotFoundException(String userId) {
super("User not found with ID: " + userId);
this.userId = userId;
}
public String getUserId() {
return userId;
}
}
// Usage - no need to declare or catch (but you can)
public User findUser(String userId) {
User user = database.find(userId);
if (user == null) {
throw new UserNotFoundException(userId);
}
return user;
}
Exception Hierarchy for Your App
// Base exception for your application
public class ApplicationException extends Exception {
public ApplicationException(String message) {
super(message);
}
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
}
// Specific exceptions extend the base
public class UserException extends ApplicationException {
public UserException(String message) {
super(message);
}
}
public class PaymentException extends ApplicationException {
public PaymentException(String message) {
super(message);
}
}
public class UserNotFoundException extends UserException {
public UserNotFoundException(String userId) {
super("User not found: " + userId);
}
}
// Now you can catch broadly or specifically
try {
processPayment(user, amount);
} catch (UserException e) {
// Handle all user-related errors
} catch (PaymentException e) {
// Handle all payment-related errors
} catch (ApplicationException e) {
// Handle any application error
}
Common Exceptions & How to Handle Them
NullPointerException
// PROBLEM: Accessing method/property on null
String name = null;
name.length(); // NullPointerException!
// SOLUTION 1: Check for null
if (name != null) {
int length = name.length();
}
// SOLUTION 2: Use Optional (modern Java)
Optional<String> maybeName = Optional.ofNullable(name);
int length = maybeName.map(String::length).orElse(0);
// SOLUTION 3: Use Objects.requireNonNull for validation
public void setName(String name) {
this.name = Objects.requireNonNull(name, "Name cannot be null");
}
ArrayIndexOutOfBoundsException
// PROBLEM: Accessing array with invalid index
int[] numbers = {1, 2, 3};
int value = numbers[5]; // ArrayIndexOutOfBoundsException!
// SOLUTION: Check bounds before accessing
if (index >= 0 && index < numbers.length) {
int value = numbers[index];
}
NumberFormatException
// PROBLEM: Parsing invalid number string
String input = "abc";
int number = Integer.parseInt(input); // NumberFormatException!
// SOLUTION: Catch and provide default
public int parseNumber(String input, int defaultValue) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
return defaultValue;
}
}
// Or validate first
public boolean isValidNumber(String input) {
return input != null && input.matches("-?\\d+");
}
IOException
// PROBLEM: File operations can fail
public String readFile(String path) {
try (BufferedReader reader = Files.newBufferedReader(Paths.get(path))) {
return reader.lines().collect(Collectors.joining("\n"));
} catch (NoSuchFileException e) {
System.out.println("File not found: " + path);
return "";
} catch (AccessDeniedException e) {
System.out.println("No permission to read: " + path);
return "";
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
return "";
}
}
Exception Handling Best Practices
1. Don't Catch Generic Exception
// BAD: Catches everything including bugs
try {
processData();
} catch (Exception e) {
System.out.println("Something went wrong");
}
// GOOD: Catch specific exceptions
try {
processData();
} catch (FileNotFoundException e) {
System.out.println("Data file not found");
} catch (DataFormatException e) {
System.out.println("Invalid data format");
}
2. Don't Swallow Exceptions
// TERRIBLE: Exception completely ignored
try {
riskyOperation();
} catch (Exception e) {
// Do nothing - silent failure!
}
// BAD: Just printing isn't enough
try {
riskyOperation();
} catch (Exception e) {
e.printStackTrace(); // Then what?
}
// GOOD: Handle appropriately or rethrow
try {
riskyOperation();
} catch (IOException e) {
logger.error("Operation failed: " + e.getMessage(), e);
throw new ServiceException("Could not complete operation", e);
}
3. Include Context in Exception Messages
// BAD: Useless message
throw new Exception("Error");
// GOOD: Helpful message with context
throw new UserNotFoundException(
"User with ID '" + userId + "' not found in database '" + dbName + "'"
);
// GOOD: Include what was attempted and what went wrong
throw new PaymentException(
"Failed to process payment of $" + amount +
" for order #" + orderId +
": " + e.getMessage()
);
4. Preserve the Original Exception
// BAD: Original exception lost
try {
database.save(user);
} catch (SQLException e) {
throw new DataException("Failed to save user"); // Original cause lost!
}
// GOOD: Chain exceptions
try {
database.save(user);
} catch (SQLException e) {
throw new DataException("Failed to save user", e); // Cause preserved
}
5. Use Exceptions for Exceptional Cases
// BAD: Using exceptions for flow control
public boolean userExists(String email) {
try {
findUser(email);
return true;
} catch (UserNotFoundException e) {
return false;
}
}
// GOOD: Use normal control flow
public boolean userExists(String email) {
return database.countByEmail(email) > 0;
}
// Exceptions are for UNEXPECTED situations, not normal conditions
6. Document Exceptions
/**
* Withdraws money from the account.
*
* @param amount the amount to withdraw (must be positive)
* @throws IllegalArgumentException if amount is negative or zero
* @throws InsufficientFundsException if balance is less than amount
*/
public void withdraw(double amount)
throws IllegalArgumentException, InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException(balance, amount);
}
balance -= amount;
}
7. Clean Up Resources Properly
// ALWAYS use try-with-resources for closeable resources
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
// process results
}
} // Everything closed automatically, even if exception occurs
Real-World Example: User Registration
// Custom exceptions
public class RegistrationException extends Exception {
public RegistrationException(String message) {
super(message);
}
public RegistrationException(String message, Throwable cause) {
super(message, cause);
}
}
public class EmailAlreadyExistsException extends RegistrationException {
public EmailAlreadyExistsException(String email) {
super("Email already registered: " + email);
}
}
public class InvalidEmailException extends RegistrationException {
public InvalidEmailException(String email) {
super("Invalid email format: " + email);
}
}
public class WeakPasswordException extends RegistrationException {
public WeakPasswordException(String reason) {
super("Password is too weak: " + reason);
}
}
// Registration service
public class UserRegistrationService {
private final UserRepository userRepository;
private final EmailValidator emailValidator;
private final PasswordValidator passwordValidator;
public User register(String email, String password, String name)
throws RegistrationException {
// Validate email format
if (!emailValidator.isValid(email)) {
throw new InvalidEmailException(email);
}
// Check if email already exists
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
// Validate password strength
ValidationResult passwordResult = passwordValidator.validate(password);
if (!passwordResult.isValid()) {
throw new WeakPasswordException(passwordResult.getReason());
}
// Create user
try {
User user = new User(email, hashPassword(password), name);
return userRepository.save(user);
} catch (DatabaseException e) {
throw new RegistrationException("Failed to create user account", e);
}
}
}
// Controller handling the registration
public class RegistrationController {
public Response register(RegistrationRequest request) {
try {
User user = registrationService.register(
request.getEmail(),
request.getPassword(),
request.getName()
);
return Response.success("Registration successful", user);
} catch (EmailAlreadyExistsException e) {
return Response.error(409, "This email is already registered");
} catch (InvalidEmailException e) {
return Response.error(400, "Please enter a valid email address");
} catch (WeakPasswordException e) {
return Response.error(400, e.getMessage());
} catch (RegistrationException e) {
logger.error("Registration failed", e);
return Response.error(500, "Registration failed. Please try again.");
}
}
}