Java Exception Handling - Complete Tutorial
Master Java exception handling: Learn try-catch-finally blocks, throw, throws, custom exceptions, exception hierarchy, and best practices for robust error management.
Exception Types
Checked & Unchecked
Try-Catch-Finally
Error handling blocks
Throw/Throws
Exception propagation
Custom Exceptions
User-defined errors
1. Introduction to Exception Handling
Exception handling in Java is a powerful mechanism to handle runtime errors so that normal flow of the application can be maintained. An exception is an event that disrupts the normal flow of the program.
Why Exception Handling?
- Maintain Normal Flow: Prevent application crashes
- Meaningful Error Messages: Provide user-friendly errors
- Resource Management: Ensure proper resource cleanup
- Debugging: Easier identification of error sources
- Robust Applications: Create fault-tolerant software
- Separation of Concerns: Separate error handling from business logic
Key Terminology
- Exception: Abnormal condition disrupting program flow
- Error: Serious problem beyond program control
- Throwable: Superclass of all exceptions and errors
- Checked Exception: Must be handled at compile time
- Unchecked Exception: Runtime exceptions
- Try-Catch: Blocks to handle exceptions
Exception Handling Flow
When an exception occurs: JVM creates exception object → Looks for handler → If found, executes handler → If not, terminates program. Proper handling prevents termination.
Exception Hierarchy
Throwable
├── Error (Serious, Unchecked)
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ └── ...
│
└── Exception
├── RuntimeException (Unchecked)
│ ├── NullPointerException
│ ├── ArithmeticException
│ ├── ArrayIndexOutOfBoundsException
│ ├── IllegalArgumentException
│ └── ...
│
└── Checked Exceptions
├── IOException
├── SQLException
├── ClassNotFoundException
└── ...
2. Exception Hierarchy and Types
Java exceptions are organized in a hierarchy with Throwable at the top. Understanding this hierarchy is crucial for effective exception handling.
| Exception Type | Superclass | Checked/Unchecked | When It Occurs | Common Examples |
|---|---|---|---|---|
| Error | Throwable | Unchecked | Serious system problems | OutOfMemoryError, StackOverflowError |
| RuntimeException | Exception | Unchecked | Programming mistakes | NullPointerException, ArithmeticException |
| Checked Exceptions | Exception (excluding RuntimeException) | Checked | External conditions | IOException, SQLException |
Checked Exceptions
- Checked at compile-time
- Must be handled using try-catch or declared with throws
- Represent recoverable conditions
- Subclasses of Exception (excluding RuntimeException)
- Examples: IOException, SQLException, ClassNotFoundException
- Client should recover from these
Unchecked Exceptions
- Checked at runtime
- Not required to be handled or declared
- Represent programming bugs
- Subclasses of RuntimeException and Error
- Examples: NullPointerException, ArrayIndexOutOfBoundsException
- Client cannot reasonably recover
public class CommonExceptionsExample {
public static void main(String[] args) {
System.out.println("=== Common Java Exceptions ===");
// 1. NullPointerException (Unchecked)
try {
String str = null;
System.out.println("Length: " + str.length()); // This will throw NPE
} catch (NullPointerException e) {
System.out.println("1. NullPointerException caught: " + e.getMessage());
}
// 2. ArithmeticException (Unchecked)
try {
int result = 10 / 0; // Division by zero
} catch (ArithmeticException e) {
System.out.println("2. ArithmeticException caught: " + e.getMessage());
}
// 3. ArrayIndexOutOfBoundsException (Unchecked)
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // Invalid index
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("3. ArrayIndexOutOfBoundsException caught: " + e.getMessage());
}
// 4. NumberFormatException (Unchecked)
try {
String invalidNumber = "abc123";
int num = Integer.parseInt(invalidNumber);
} catch (NumberFormatException e) {
System.out.println("4. NumberFormatException caught: " + e.getMessage());
}
// 5. IllegalArgumentException (Unchecked)
try {
setAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("5. IllegalArgumentException caught: " + e.getMessage());
}
// 6. ClassCastException (Unchecked)
try {
Object obj = "Hello";
Integer num = (Integer) obj; // Invalid cast
} catch (ClassCastException e) {
System.out.println("6. ClassCastException caught: " + e.getMessage());
}
// 7. StringIndexOutOfBoundsException (Unchecked)
try {
String text = "Hello";
char ch = text.charAt(10); // Invalid string index
} catch (StringIndexOutOfBoundsException e) {
System.out.println("7. StringIndexOutOfBoundsException caught: " + e.getMessage());
}
// 8. NegativeArraySizeException (Unchecked)
try {
int[] arr = new int[-5]; // Negative array size
} catch (NegativeArraySizeException e) {
System.out.println("8. NegativeArraySizeException caught: " + e.getMessage());
}
System.out.println("\nProgram continues normally after handling exceptions!");
}
private static void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
System.out.println("Age set to: " + age);
}
}
3. Try-Catch-Finally Blocks
The try-catch-finally blocks are the core of Java exception handling. They allow you to write code that might throw exceptions and handle them gracefully.
Try-Catch-Finally Flow
Start
│
▼
┌─────────┐
│ try │ ←── Exception occurs here
└────┬────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Exception Exception No Exception
A B │
│ │ │
└─────┬─────┘ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────┐
│ catch block │ │finally │
│ (handles) │ │ block │
└────────┬────────┘ └────┬────┘
│ │
└────────┬────────┘
│
▼
Continue program
import java.util.Scanner;
public class TryCatchFinallyExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("=== Try-Catch-Finally Examples ===");
// Example 1: Basic try-catch
System.out.println("\n--- Example 1: Basic Try-Catch ---");
try {
System.out.print("Enter numerator: ");
int numerator = scanner.nextInt();
System.out.print("Enter denominator: ");
int denominator = scanner.nextInt();
int result = numerator / denominator;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Error: Division by zero is not allowed!");
} catch (Exception e) {
System.out.println("Unexpected error: " + e.getMessage());
}
// Example 2: Multiple catch blocks
System.out.println("\n--- Example 2: Multiple Catch Blocks ---");
try {
System.out.print("Enter array size: ");
int size = scanner.nextInt();
int[] array = new int[size];
System.out.print("Enter index to access: ");
int index = scanner.nextInt();
System.out.print("Enter value to store: ");
String value = scanner.next();
array[index] = Integer.parseInt(value);
System.out.println("Value stored successfully!");
} catch (NegativeArraySizeException e) {
System.out.println("Error: Array size cannot be negative!");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Error: Invalid array index!");
} catch (NumberFormatException e) {
System.out.println("Error: Invalid number format!");
} catch (Exception e) {
System.out.println("General error: " + e.getMessage());
}
// Example 3: Try-catch-finally with resources
System.out.println("\n--- Example 3: Try-Catch-Finally ---");
Scanner fileScanner = null;
try {
System.out.print("Enter filename to open: ");
String filename = scanner.next();
// Simulating file operations
if (filename.equals("missing.txt")) {
throw new RuntimeException("File not found: " + filename);
}
System.out.println("Processing file: " + filename);
// File processing logic here
} catch (RuntimeException e) {
System.out.println("File error: " + e.getMessage());
} finally {
// This block ALWAYS executes
System.out.println("Finally block executed - cleaning up resources");
if (fileScanner != null) {
fileScanner.close();
}
}
// Example 4: Nested try-catch
System.out.println("\n--- Example 4: Nested Try-Catch ---");
try {
// Outer try block
System.out.println("Outer try block started");
try {
// Inner try block
System.out.print("Enter a number: ");
int num = scanner.nextInt();
if (num < 0) {
throw new IllegalArgumentException("Negative numbers not allowed");
}
System.out.println("Square root: " + Math.sqrt(num));
} catch (IllegalArgumentException e) {
System.out.println("Inner catch: " + e.getMessage());
throw e; // Re-throwing exception
} finally {
System.out.println("Inner finally executed");
}
System.out.println("Outer try block completed");
} catch (Exception e) {
System.out.println("Outer catch: " + e.getClass().getSimpleName());
} finally {
System.out.println("Outer finally executed");
}
// Example 5: Try-with-resources (Java 7+)
System.out.println("\n--- Example 5: Try-With-Resources ---");
try (Scanner autoCloseScanner = new Scanner(System.in)) {
System.out.print("Enter your name: ");
String name = autoCloseScanner.nextLine();
System.out.println("Hello, " + name + "!");
// Scanner auto-closes here
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("\n=== Program Completed Successfully ===");
scanner.close();
}
// Helper method demonstrating exception propagation
private static void riskyMethod() throws Exception {
System.out.println("Executing risky method...");
if (Math.random() > 0.5) {
throw new Exception("Something went wrong!");
}
System.out.println("Risky method completed successfully");
}
}
| Block | Purpose | When It Executes | Required? | Multiple Allowed? |
|---|---|---|---|---|
try |
Contains code that might throw exceptions | Always (if no exception before) | Yes | No (but can be nested) |
catch |
Handles specific exceptions | Only when matching exception occurs | No (but try must have catch or finally) | Yes (multiple catch blocks) |
finally |
Cleanup code (always executes) | Always (except System.exit()) | No | No (only one finally per try) |
- Always catch more specific exceptions first
- More general exceptions (
Exception) should come last - Compiler error if specific exception comes after general one
- Example:
catch(ArithmeticException e)beforecatch(Exception e) - Unreachable catch blocks cause compilation errors
4. Throw and Throws Keywords
The throw keyword is used to explicitly throw an exception, while throws is used in method signatures to declare exceptions that the method might throw.
public class ThrowExample {
// Method that throws exception based on condition
public static void checkAge(int age) {
if (age < 18) {
// Explicitly throwing an exception
throw new IllegalArgumentException(
"Age must be 18 or older. Provided: " + age
);
}
System.out.println("Age verified: " + age);
}
// Method that throws different types of exceptions
public static void processInput(String input, int divisor) {
if (input == null || input.trim().isEmpty()) {
throw new NullPointerException("Input cannot be null or empty");
}
if (divisor == 0) {
throw new ArithmeticException("Divisor cannot be zero");
}
if (!input.matches("\\d+")) {
throw new NumberFormatException("Input must be a number: " + input);
}
int number = Integer.parseInt(input);
System.out.println("Result: " + (number / divisor));
}
// Method that creates and throws custom exception
public static void withdrawMoney(double balance, double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Withdrawal amount must be positive: " + amount
);
}
if (amount > balance) {
// Creating exception with detailed message
InsufficientFundsException exception =
new InsufficientFundsException(balance, amount);
throw exception;
}
balance -= amount;
System.out.println("Withdrawal successful. New balance: " + balance);
}
// Method that re-throws exception
public static void processWithRetry(int attempts) {
for (int i = 1; i <= attempts; i++) {
try {
System.out.println("Attempt " + i + " of " + attempts);
riskyOperation();
System.out.println("Operation successful!");
return; // Exit if successful
} catch (RuntimeException e) {
System.out.println("Attempt " + i + " failed: " + e.getMessage());
if (i == attempts) {
// Re-throw the exception on last attempt
throw new RuntimeException(
"All " + attempts + " attempts failed", e
);
}
// Wait before retry
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
private static void riskyOperation() {
if (Math.random() < 0.7) {
throw new RuntimeException("Random operation failure");
}
}
public static void main(String[] args) {
System.out.println("=== Throw Keyword Examples ===");
// Example 1: Basic throw
try {
checkAge(16); // This will throw exception
} catch (IllegalArgumentException e) {
System.out.println("Caught: " + e.getMessage());
}
// Example 2: Multiple throw scenarios
try {
processInput("abc", 5); // Will throw NumberFormatException
} catch (Exception e) {
System.out.println("Caught: " + e.getClass().getSimpleName() +
" - " + e.getMessage());
}
// Example 3: Custom exception throw
try {
withdrawMoney(1000.0, 1500.0); // Insufficient funds
} catch (InsufficientFundsException e) {
System.out.println("Caught: " + e.getMessage());
System.out.println("Required: " + e.getRequiredAmount());
System.out.println("Available: " + e.getAvailableBalance());
}
// Example 4: Re-throw with wrapper
try {
processWithRetry(3);
} catch (RuntimeException e) {
System.out.println("Final error: " + e.getMessage());
System.out.println("Original cause: " + e.getCause().getMessage());
}
// Example 5: Throw in constructor
try {
Person p = new Person("", 25); // Empty name
} catch (IllegalArgumentException e) {
System.out.println("Person creation failed: " + e.getMessage());
}
System.out.println("\nProgram continues after exception handling");
}
}
// Custom exception class
class InsufficientFundsException extends RuntimeException {
private double availableBalance;
private double requiredAmount;
public InsufficientFundsException(double balance, double amount) {
super("Insufficient funds. Available: " + balance + ", Required: " + amount);
this.availableBalance = balance;
this.requiredAmount = amount;
}
public double getAvailableBalance() {
return availableBalance;
}
public double getRequiredAmount() {
return requiredAmount;
}
}
// Class demonstrating throw in constructor
class Person {
private String name;
private int age;
public Person(String name, int age) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.name = name;
this.age = age;
}
}
import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.sql.*;
public class ThrowsExample {
// Method declaring checked exceptions with throws
public static void readFile(String filename)
throws FileNotFoundException, IOException {
System.out.println("Reading file: " + filename);
// This can throw FileNotFoundException
FileReader fileReader = new FileReader(filename);
BufferedReader reader = new BufferedReader(fileReader);
try {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} finally {
reader.close();
}
}
// Method declaring multiple exceptions
public static void connectToDatabase(String url, String user, String password)
throws SQLException, ClassNotFoundException {
System.out.println("Connecting to database...");
// This can throw ClassNotFoundException
Class.forName("com.mysql.cj.jdbc.Driver");
// This can throw SQLException
Connection connection = DriverManager.getConnection(url, user, password);
try {
// Database operations
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
while (resultSet.next()) {
System.out.println("User: " + resultSet.getString("username"));
}
} finally {
connection.close();
}
}
// Method with both checked and unchecked exceptions
public static void processUrl(String urlString)
throws MalformedURLException, IOException {
System.out.println("Processing URL: " + urlString);
// Can throw MalformedURLException (checked)
URL url = new URL(urlString);
// Can throw IOException (checked)
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream())
);
try {
String line;
int lineCount = 0;
while ((line = reader.readLine()) != null && lineCount < 10) {
System.out.println("Line " + (++lineCount) + ": " + line);
}
} finally {
reader.close();
}
}
// Method that doesn't handle exceptions but declares them
public static void riskyOperation()
throws IOException, SQLException, ClassNotFoundException {
double random = Math.random();
if (random < 0.33) {
throw new IOException("Simulated IO error");
} else if (random < 0.66) {
throw new SQLException("Simulated database error");
} else {
throw new ClassNotFoundException("Simulated class not found");
}
}
// Method with throws in interface implementation
interface DataProcessor {
void process() throws IOException, SQLException;
}
static class FileProcessor implements DataProcessor {
@Override
public void process() throws IOException {
// Can only throw IOException or its subclasses
// Cannot throw SQLException even though interface declares it
throw new IOException("File processing error");
}
}
// Main method with throws declaration
public static void main(String[] args)
throws IOException, SQLException, ClassNotFoundException {
System.out.println("=== Throws Keyword Examples ===");
// Example 1: Handling declared exceptions
try {
readFile("test.txt");
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.out.println("IO Error: " + e.getMessage());
}
// Example 2: Propagating exceptions
try {
connectToDatabase(
"jdbc:mysql://localhost:3306/mydb",
"root",
"password"
);
} catch (SQLException | ClassNotFoundException e) {
System.out.println("Database error: " + e.getClass().getSimpleName());
// Re-throw with additional context
throw new RuntimeException("Database operation failed", e);
}
// Example 3: Multiple exception handling
String[] urls = {
"https://example.com",
"invalid-url", // Will cause MalformedURLException
"https://google.com"
};
for (String url : urls) {
try {
processUrl(url);
} catch (MalformedURLException e) {
System.out.println("Invalid URL: " + url + " - " + e.getMessage());
} catch (IOException e) {
System.out.println("IO Error processing URL: " + e.getMessage());
}
}
// Example 4: Using method with throws
try {
riskyOperation();
} catch (IOException | SQLException | ClassNotFoundException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
// Can choose to handle or re-throw
}
// Example 5: Method overriding with throws
DataProcessor processor = new FileProcessor();
try {
processor.process();
} catch (IOException e) {
System.out.println("Processor error: " + e.getMessage());
}
System.out.println("\n=== Program Completed ===");
}
// Helper method demonstrating exception propagation chain
public static void methodA() throws IOException {
System.out.println("Method A calling Method B");
methodB(); // Exception propagates up
}
public static void methodB() throws IOException {
System.out.println("Method B calling Method C");
methodC(); // Exception propagates up
}
public static void methodC() throws IOException {
System.out.println("Method C throwing exception");
throw new IOException("Error originated in Method C");
}
}
| Keyword | Purpose | Used In | Exception Type | Example |
|---|---|---|---|---|
throw |
Explicitly throw an exception object | Method body | Any Throwable | throw new IOException(); |
throws |
Declare exceptions method might throw | Method signature | Checked exceptions | void read() throws IOException |
- Use throws when the current method cannot handle the exception
- Use try-catch when you can handle the exception locally
- Checked exceptions must be either caught or declared with throws
- Unchecked exceptions can be thrown without declaration
- Overriding methods can throw same or narrower exceptions than parent
- Overriding methods cannot throw broader checked exceptions
5. Creating Custom Exceptions
Custom exceptions allow you to create application-specific exception types that provide meaningful error information for your domain.
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
public class CustomExceptionsExample {
public static void main(String[] args) {
System.out.println("=== Custom Exception Examples ===");
// Example 1: Basic custom exception
System.out.println("\n--- Example 1: Authentication ---");
try {
authenticateUser("admin", "wrongpassword");
} catch (AuthenticationException e) {
System.out.println("Authentication failed:");
System.out.println("Error Code: " + e.getErrorCode());
System.out.println("Message: " + e.getMessage());
System.out.println("Timestamp: " + e.getTimestamp());
}
// Example 2: Validation exception with details
System.out.println("\n--- Example 2: User Registration ---");
try {
registerUser("", "weak", "user@example.com", 15);
} catch (ValidationException e) {
System.out.println("Validation failed:");
System.out.println("Errors: " + e.getErrors());
e.getErrors().forEach((field, error) ->
System.out.println(field + ": " + error)
);
}
// Example 3: Business logic exception
System.out.println("\n--- Example 3: Bank Transaction ---");
try {
BankAccount account = new BankAccount("ACC001", 500.0);
account.withdraw(800.0);
} catch (InsufficientBalanceException e) {
System.out.println("Transaction failed:");
System.out.println("Account: " + e.getAccountNumber());
System.out.println("Available: $" + e.getAvailableBalance());
System.out.println("Requested: $" + e.getRequestedAmount());
System.out.println("Shortage: $" + e.getShortageAmount());
}
// Example 4: Chained custom exceptions
System.out.println("\n--- Example 4: Order Processing ---");
try {
processOrder("ORD123", -2);
} catch (OrderProcessingException e) {
System.out.println("Order processing error:");
System.out.println("Order ID: " + e.getOrderId());
System.out.println("Error: " + e.getMessage());
if (e.getCause() != null) {
System.out.println("Root cause: " + e.getCause().getMessage());
}
}
// Example 5: Checked custom exception
System.out.println("\n--- Example 5: File Processing ---");
try {
processFile("encrypted.dat");
} catch (FileEncryptionException e) {
System.out.println("File processing error:");
System.out.println("File: " + e.getFileName());
System.out.println("Error: " + e.getMessage());
System.out.println("Suggestion: " + e.getRecoverySuggestion());
}
System.out.println("\n=== All Examples Completed ===");
}
// Example 1: Basic custom unchecked exception
static void authenticateUser(String username, String password) {
if (!"admin".equals(username) || !"admin123".equals(password)) {
throw new AuthenticationException(
"INVALID_CREDENTIALS",
"Invalid username or password"
);
}
System.out.println("Authentication successful for: " + username);
}
// Example 2: Validation with multiple errors
static void registerUser(String username, String password,
String email, int age) {
Map errors = new HashMap<>();
if (username == null || username.trim().isEmpty()) {
errors.put("username", "Username cannot be empty");
} else if (username.length() < 3) {
errors.put("username", "Username must be at least 3 characters");
}
if (password == null || password.length() < 8) {
errors.put("password", "Password must be at least 8 characters");
}
if (email == null || !email.contains("@")) {
errors.put("email", "Invalid email address");
}
if (age < 18) {
errors.put("age", "User must be at least 18 years old");
}
if (!errors.isEmpty()) {
throw new ValidationException("User validation failed", errors);
}
System.out.println("User registered successfully: " + username);
}
// Example 3: Business logic with custom exception
static class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Withdrawal amount must be positive"
);
}
if (amount > balance) {
throw new InsufficientBalanceException(
accountNumber, balance, amount
);
}
balance -= amount;
System.out.println("Withdrawn: $" + amount +
", New balance: $" + balance);
}
}
// Example 4: Chained exceptions
static void processOrder(String orderId, int quantity) {
try {
if (quantity <= 0) {
throw new IllegalArgumentException(
"Quantity must be positive: " + quantity
);
}
// Simulate inventory check
if (quantity > 100) {
throw new RuntimeException("Insufficient inventory");
}
System.out.println("Processing order: " + orderId);
} catch (Exception e) {
// Wrap the exception with custom exception
throw new OrderProcessingException(
orderId,
"Failed to process order: " + orderId,
e // Preserve original cause
);
}
}
// Example 5: Checked custom exception
static void processFile(String fileName) throws FileEncryptionException {
try {
// Simulate file processing
if (fileName.endsWith(".encrypted")) {
throw new IOException("Decryption key not found");
}
System.out.println("Processing file: " + fileName);
} catch (IOException e) {
throw new FileEncryptionException(
fileName,
"Failed to process encrypted file",
e,
"Please provide decryption key or use unencrypted file"
);
}
}
}
// ========== CUSTOM EXCEPTION CLASSES ==========
// 1. Basic custom unchecked exception
class AuthenticationException extends RuntimeException {
private String errorCode;
private LocalDateTime timestamp;
public AuthenticationException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
}
public String getErrorCode() {
return errorCode;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
}
// 2. Validation exception with error details
class ValidationException extends RuntimeException {
private Map errors;
public ValidationException(String message, Map errors) {
super(message);
this.errors = errors;
}
public Map getErrors() {
return errors;
}
@Override
public String toString() {
return super.toString() + ", Errors: " + errors;
}
}
// 3. Business exception with account details
class InsufficientBalanceException extends RuntimeException {
private String accountNumber;
private double availableBalance;
private double requestedAmount;
public InsufficientBalanceException(String accountNumber,
double balance, double amount) {
super(String.format(
"Insufficient balance in account %s. Available: $%.2f, Requested: $%.2f",
accountNumber, balance, amount
));
this.accountNumber = accountNumber;
this.availableBalance = balance;
this.requestedAmount = amount;
}
public String getAccountNumber() {
return accountNumber;
}
public double getAvailableBalance() {
return availableBalance;
}
public double getRequestedAmount() {
return requestedAmount;
}
public double getShortageAmount() {
return requestedAmount - availableBalance;
}
}
// 4. Exception with order context
class OrderProcessingException extends RuntimeException {
private String orderId;
public OrderProcessingException(String orderId, String message) {
super(message);
this.orderId = orderId;
}
public OrderProcessingException(String orderId, String message, Throwable cause) {
super(message, cause);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
// 5. Checked custom exception with recovery suggestion
class FileEncryptionException extends Exception {
private String fileName;
private String recoverySuggestion;
public FileEncryptionException(String fileName, String message) {
super(message);
this.fileName = fileName;
}
public FileEncryptionException(String fileName, String message,
Throwable cause) {
super(message, cause);
this.fileName = fileName;
}
public FileEncryptionException(String fileName, String message,
Throwable cause, String recoverySuggestion) {
super(message, cause);
this.fileName = fileName;
this.recoverySuggestion = recoverySuggestion;
}
public String getFileName() {
return fileName;
}
public String getRecoverySuggestion() {
return recoverySuggestion;
}
}
When to Create Custom Exceptions
- Domain-specific error conditions
- Need to include additional information
- Better error categorization
- Consistent error handling across application
- When built-in exceptions don't accurately describe error
- For business rule violations
Custom Exception Best Practices
- Extend appropriate superclass (Exception or RuntimeException)
- Provide meaningful error messages
- Include serialVersionUID for Serializable exceptions
- Add constructors matching superclass patterns
- Include relevant context information
- Follow naming convention (ends with "Exception")
- Overuse: Don't create custom exceptions for every minor error
- Empty exceptions: Always provide meaningful messages
- Ignoring cause: Preserve original exception with
initCause()or constructor - Checked for everything: Use runtime exceptions for programming errors
- Too many fields: Keep exceptions focused and simple
6. Exception Handling Best Practices
- Empty catch blocks:
catch(Exception e) {} - Catching Throwable/Error: Don't catch serious system errors
- Overly broad catch:
catch(Exception e)when specific would do - Exception swallowing: Hiding exceptions without logging
- Resource leaks: Not closing resources in finally block
- Using exceptions for flow control: Exceptions are for exceptional conditions
Exception Handling Best Practices
- Use specific exceptions over general ones
- Document exceptions with @throws javadoc
- Use try-with-resources for auto-closeable resources
- Clean up resources in finally blocks
- Include cause exceptions when re-throwing
- Use runtime exceptions for programming errors
- Log exceptions appropriately
Performance Considerations
- Exception creation is expensive - avoid in loops
- Use precondition checks to avoid exceptions
- Consider error codes for performance-critical code
- Cache and reuse exception objects when safe
- Use boolean return values for expected failures
- Fill in stack trace only when needed
Exception Handling Principles
Fail Fast: Detect errors as early as possible
Fail Safe: System remains in safe state after failure
Separation of Concerns: Keep error handling separate from business logic
Meaningful Messages: Provide clear, actionable error information
Graceful Degradation: System continues with reduced functionality
Defensive Programming: Assume everything that can go wrong will
import java.io.*;
import java.util.logging.*;
public class BestPracticesExample {
private static final Logger logger =
Logger.getLogger(BestPracticesExample.class.getName());
public static void main(String[] args) {
System.out.println("=== Exception Handling Best Practices ===");
// Practice 1: Specific exceptions over general ones
try {
readConfiguration("config.properties");
} catch (FileNotFoundException e) {
// Specific handling for missing file
logger.severe("Configuration file not found: " + e.getMessage());
System.out.println("Using default configuration");
} catch (IOException e) {
// General IO errors
logger.severe("IO error reading configuration: " + e.getMessage());
}
// Practice 2: Try-with-resources
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
} catch (IOException e) {
logger.log(Level.SEVERE, "Error reading file", e);
}
// Practice 3: Never swallow exceptions
badPracticeSwallowException();
goodPracticeLogException();
// Practice 4: Use exceptions for exceptional conditions
validateInputBeforeProcessing("valid input");
// Practice 5: Include cause when re-throwing
try {
processData();
} catch (ProcessingException e) {
logger.log(Level.SEVERE, "Data processing failed", e);
// Original cause is preserved
System.out.println("Root cause: " + e.getCause().getMessage());
}
// Practice 6: Clean up in finally or try-with-resources
Connection connection = null;
try {
connection = openDatabaseConnection();
executeQuery(connection);
} catch (SQLException e) {
logger.severe("Database error: " + e.getMessage());
} finally {
// Always close resources
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warning("Error closing connection: " + e.getMessage());
}
}
}
System.out.println("\n=== Program Completed ===");
}
// GOOD: Specific exception handling
private static void readConfiguration(String filename)
throws IOException {
// Business logic here
}
// BAD: Swallowing exception
private static void badPracticeSwallowException() {
try {
riskyOperation();
} catch (Exception e) {
// BAD: Exception completely swallowed
// No logging, no re-throw, no recovery
}
}
// GOOD: Proper exception logging
private static void goodPracticeLogException() {
try {
riskyOperation();
} catch (Exception e) {
// GOOD: Log the exception
logger.log(Level.SEVERE, "Operation failed", e);
// Either recover or re-throw
throw new RuntimeException("Operation failed", e);
}
}
// GOOD: Validate before processing (avoid exceptions)
private static void validateInputBeforeProcessing(String input) {
if (input == null || input.trim().isEmpty()) {
// Return error instead of throwing exception
System.out.println("Invalid input");
return;
}
if (input.length() > 100) {
System.out.println("Input too long");
return;
}
// Process valid input
System.out.println("Processing: " + input);
}
// GOOD: Preserve cause when wrapping exceptions
private static void processData() throws ProcessingException {
try {
// Some operation that might fail
parseComplexData();
} catch (IOException | NumberFormatException e) {
// Wrap with custom exception, preserving cause
throw new ProcessingException("Failed to process data", e);
}
}
// Helper methods (simulated)
private static void riskyOperation() {
throw new RuntimeException("Simulated failure");
}
private static void processLine(String line) {
// Process line
}
private static Connection openDatabaseConnection() throws SQLException {
return new Connection(); // Simulated
}
private static void executeQuery(Connection connection) throws SQLException {
// Execute query
}
private static void parseComplexData() throws IOException {
throw new IOException("Simulated parse error");
}
}
// Custom exception for practice 5
class ProcessingException extends Exception {
public ProcessingException(String message) {
super(message);
}
public ProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
// Simulated classes
class Connection implements AutoCloseable {
public void close() throws SQLException {
// Close connection
}
}
class SQLException extends Exception {
public SQLException(String message) {
super(message);
}
}
7. Real-World Exception Handling Examples
Example 1: E-commerce Order Processing
import java.util.*;
public class ECommerceExample {
public static void main(String[] args) {
System.out.println("=== E-commerce Order Processing ===");
try {
// Simulate order processing
Order order = createOrder("ORD001", Arrays.asList(
new OrderItem("PROD001", 2, 25.99),
new OrderItem("PROD002", 1, 99.99),
new OrderItem("PROD003", 0, 49.99) // Zero quantity
));
processOrder(order);
System.out.println("Order processed successfully!");
} catch (OrderValidationException e) {
System.out.println("Order validation failed:");
System.out.println("Order ID: " + e.getOrderId());
System.out.println("Errors:");
e.getValidationErrors().forEach((item, error) ->
System.out.println(" " + item + ": " + error)
);
} catch (PaymentProcessingException e) {
System.out.println("Payment processing failed:");
System.out.println("Order ID: " + e.getOrderId());
System.out.println("Amount: $" + e.getAmount());
System.out.println("Error: " + e.getMessage());
System.out.println("Suggestion: " + e.getRecoverySuggestion());
} catch (InventoryException e) {
System.out.println("Inventory issue:");
System.out.println("Product: " + e.getProductId());
System.out.println("Requested: " + e.getRequestedQuantity());
System.out.println("Available: " + e.getAvailableQuantity());
if (e.isBackOrderAvailable()) {
System.out.println("Product available for backorder");
}
} catch (ShippingException e) {
System.out.println("Shipping error:");
System.out.println("Address: " + e.getShippingAddress());
System.out.println("Error: " + e.getMessage());
} catch (Exception e) {
System.out.println("Unexpected error: " + e.getMessage());
logger.severe("Order processing failed", e);
}
}
static Order createOrder(String orderId, List items)
throws OrderValidationException {
Map errors = new HashMap<>();
if (orderId == null || orderId.trim().isEmpty()) {
errors.put("orderId", "Order ID cannot be empty");
}
if (items == null || items.isEmpty()) {
errors.put("items", "Order must contain at least one item");
} else {
for (int i = 0; i < items.size(); i++) {
OrderItem item = items.get(i);
if (item.getQuantity() <= 0) {
errors.put("item_" + i,
"Quantity must be positive for product: " +
item.getProductId());
}
if (item.getUnitPrice() <= 0) {
errors.put("item_" + i,
"Price must be positive for product: " +
item.getProductId());
}
}
}
if (!errors.isEmpty()) {
throw new OrderValidationException(orderId, errors);
}
return new Order(orderId, items);
}
static void processOrder(Order order)
throws PaymentProcessingException,
InventoryException,
ShippingException {
// Step 1: Validate payment
processPayment(order);
// Step 2: Check inventory
checkInventory(order);
// Step 3: Arrange shipping
arrangeShipping(order);
// Step 4: Update inventory
updateInventory(order);
// Step 5: Send notifications
sendNotifications(order);
}
static void processPayment(Order order) throws PaymentProcessingException {
double total = order.calculateTotal();
// Simulate payment processing
boolean paymentSuccessful = Math.random() > 0.1; // 90% success rate
if (!paymentSuccessful) {
throw new PaymentProcessingException(
order.getOrderId(),
total,
"Payment declined by bank",
"Please check your payment details or use a different card"
);
}
System.out.println("Payment processed: $" + total);
}
static void checkInventory(Order order) throws InventoryException {
for (OrderItem item : order.getItems()) {
// Simulate inventory check
int availableStock = getAvailableStock(item.getProductId());
if (availableStock < item.getQuantity()) {
boolean backOrder = isBackOrderAvailable(item.getProductId());
throw new InventoryException(
item.getProductId(),
item.getQuantity(),
availableStock,
backOrder
);
}
}
System.out.println("Inventory check passed");
}
static void arrangeShipping(Order order) throws ShippingException {
String address = order.getShippingAddress();
// Simulate shipping validation
if (address == null || address.trim().isEmpty()) {
throw new ShippingException(
address,
"Shipping address cannot be empty"
);
}
if (address.contains("PO Box")) {
throw new ShippingException(
address,
"Cannot ship to PO Box addresses"
);
}
System.out.println("Shipping arranged to: " + address);
}
// Helper methods (simulated)
static int getAvailableStock(String productId) {
return (int) (Math.random() * 10); // Random stock
}
static boolean isBackOrderAvailable(String productId) {
return Math.random() > 0.5; // 50% chance
}
static void updateInventory(Order order) {
System.out.println("Inventory updated");
}
static void sendNotifications(Order order) {
System.out.println("Notifications sent");
}
static void logger(String level, String message, Exception e) {
System.out.println("[" + level + "] " + message);
if (e != null) {
System.out.println("Exception: " + e.getClass().getSimpleName());
}
}
}
// ========== MODEL CLASSES ==========
class Order {
private String orderId;
private List items;
private String shippingAddress = "123 Main St";
public Order(String orderId, List items) {
this.orderId = orderId;
this.items = items;
}
public String getOrderId() { return orderId; }
public List getItems() { return items; }
public String getShippingAddress() { return shippingAddress; }
public double calculateTotal() {
return items.stream()
.mapToDouble(item -> item.getQuantity() * item.getUnitPrice())
.sum();
}
}
class OrderItem {
private String productId;
private int quantity;
private double unitPrice;
public OrderItem(String productId, int quantity, double unitPrice) {
this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public String getProductId() { return productId; }
public int getQuantity() { return quantity; }
public double getUnitPrice() { return unitPrice; }
}
// ========== CUSTOM EXCEPTIONS ==========
class OrderValidationException extends Exception {
private String orderId;
private Map validationErrors;
public OrderValidationException(String orderId,
Map errors) {
super("Order validation failed for: " + orderId);
this.orderId = orderId;
this.validationErrors = errors;
}
public String getOrderId() { return orderId; }
public Map getValidationErrors() { return validationErrors; }
}
class PaymentProcessingException extends Exception {
private String orderId;
private double amount;
private String recoverySuggestion;
public PaymentProcessingException(String orderId, double amount,
String message, String suggestion) {
super(message);
this.orderId = orderId;
this.amount = amount;
this.recoverySuggestion = suggestion;
}
public String getOrderId() { return orderId; }
public double getAmount() { return amount; }
public String getRecoverySuggestion() { return recoverySuggestion; }
}
class InventoryException extends Exception {
private String productId;
private int requestedQuantity;
private int availableQuantity;
private boolean backOrderAvailable;
public InventoryException(String productId, int requested,
int available, boolean backOrder) {
super(String.format(
"Insufficient stock for %s. Requested: %d, Available: %d",
productId, requested, available
));
this.productId = productId;
this.requestedQuantity = requested;
this.availableQuantity = available;
this.backOrderAvailable = backOrder;
}
public String getProductId() { return productId; }
public int getRequestedQuantity() { return requestedQuantity; }
public int getAvailableQuantity() { return availableQuantity; }
public boolean isBackOrderAvailable() { return backOrderAvailable; }
}
class ShippingException extends Exception {
private String shippingAddress;
public ShippingException(String address, String message) {
super(message);
this.shippingAddress = address;
}
public String getShippingAddress() { return shippingAddress; }
}