Custom Exceptions in Java
Learn how to create your own exception types for specific errors
💡 Think of custom exceptions like creating your own alarm system with custom messages! Instead of just saying 'ERROR!', you can say 'Low Battery!' or 'Door Open!' - making it crystal clear what went wrong!
🎨 What are Custom Exceptions?
Custom exceptions are your own exception classes that extend Exception or RuntimeException. They let you create meaningful, domain-specific error types that make your code more readable and maintainable.
// Creating a simple custom exceptionclass InvalidAgeException extends Exception { // Constructor with message public InvalidAgeException(String message) { super(message); // Pass message to parent Exception class }}public class SimpleCustomException { public static void main(String[] args) { try { validateAge(15); } catch (InvalidAgeException e) { System.out.println("Caught custom exception!"); System.out.println("Message: " + e.getMessage()); } try { validateAge(25); System.out.println("Age is valid!"); } catch (InvalidAgeException e) { System.out.println("Error: " + e.getMessage()); } } public static void validateAge(int age) throws InvalidAgeException { if (age < 18) { // Throwing our custom exception! throw new InvalidAgeException( "Age " + age + " is not valid. Must be 18 or older." ); } }}// Output:// Caught custom exception!// Message: Age 15 is not valid. Must be 18 or older.// Age is valid!🔍 💡 Why Use Custom Exceptions?
Custom exceptions provide several benefits:
Clarity
Specific exception names make errors self-documenting
Organization
Group related errors under custom exception types
Context
Include relevant data and custom messages
Handling
Catch and handle different error types separately
// Without custom exception - unclear!public void transfer(Account from, Account to, double amount) throws Exception { if (from.getBalance() < amount) { throw new Exception("Error"); // What error? }}// With custom exception - crystal clear!public void transfer(Account from, Account to, double amount) throws InsufficientFundsException { if (from.getBalance() < amount) { throw new InsufficientFundsException( "Cannot transfer $" + amount + ". Available balance: $" + from.getBalance() ); }}// Benefits are obvious:// 1. Exception name tells exactly what went wrong// 2. Can catch specific exception type// 3. Can include relevant data (balance, amount)// 4. Self-documenting code💰 Real-World Example: Banking System
// Custom exception for insufficient fundsclass InsufficientFundsException extends Exception { private double balance; private double withdrawAmount; public InsufficientFundsException(double balance, double withdrawAmount) { super("Insufficient funds! Balance: $" + balance + ", Requested: $" + withdrawAmount); this.balance = balance; this.withdrawAmount = withdrawAmount; } public double getShortfall() { return withdrawAmount - balance; } public double getBalance() { return balance; }}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) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException(balance, amount); } balance -= amount; System.out.println("Withdrew $" + amount); System.out.println("New balance: $" + balance); } public double getBalance() { return balance; }}public class BankingExceptions { public static void main(String[] args) { BankAccount account = new BankAccount("ACC123", 1000.0); System.out.println("Initial balance: $" + account.getBalance()); // Valid withdrawal try { account.withdraw(200); } catch (InsufficientFundsException e) { System.out.println("Error: " + e.getMessage()); } // Invalid withdrawal - not enough funds! try { account.withdraw(2000); } catch (InsufficientFundsException e) { System.out.println("\n❌ " + e.getMessage()); System.out.println("You need $" + e.getShortfall() + " more"); } }}// Output:// Initial balance: $1000.0// Withdrew $200.0// New balance: $800.0//// ❌ Insufficient funds! Balance: $800.0, Requested: $2000.0// You need $1200.0 more🔧 Custom Exception with Multiple Constructors
// Well-designed custom exception with multiple constructorsclass InvalidEmailException extends Exception { private String email; private String reason; // Constructor 1: Just message public InvalidEmailException(String message) { super(message); } // Constructor 2: Message with cause public InvalidEmailException(String message, Throwable cause) { super(message, cause); } // Constructor 3: With additional details public InvalidEmailException(String email, String reason) { super("Invalid email '" + email + "': " + reason); this.email = email; this.reason = reason; } public String getEmail() { return email; } public String getReason() { return reason; }}public class FlexibleException { public static void main(String[] args) { // Using constructor 1 try { throw new InvalidEmailException("Email validation failed"); } catch (InvalidEmailException e) { System.out.println("Error 1: " + e.getMessage()); } // Using constructor 3 - with details try { validateEmail("invalid-email"); } catch (InvalidEmailException e) { System.out.println("\nError 2: " + e.getMessage()); System.out.println("Email: " + e.getEmail()); System.out.println("Reason: " + e.getReason()); } } public static void validateEmail(String email) throws InvalidEmailException { if (!email.contains("@")) { throw new InvalidEmailException(email, "Missing @ symbol"); } if (!email.contains(".")) { throw new InvalidEmailException(email, "Missing domain"); } }}// Output:// Error 1: Email validation failed//// Error 2: Invalid email 'invalid-email': Missing @ symbol// Email: invalid-email// Reason: Missing @ symbol🛒 Real-World Example: E-Commerce System
// Custom exceptions for e-commerceclass ProductNotFoundException extends Exception { private String productId; public ProductNotFoundException(String productId) { super("Product not found: " + productId); this.productId = productId; } public String getProductId() { return productId; }}class OutOfStockException extends Exception { private String productName; private int requestedQuantity; private int availableQuantity; public OutOfStockException(String productName, int requested, int available) { super(productName + " is out of stock. Requested: " + requested + ", Available: " + available); this.productName = productName; this.requestedQuantity = requested; this.availableQuantity = available; } public int getAvailableQuantity() { return availableQuantity; }}class InvalidQuantityException extends Exception { public InvalidQuantityException(String message) { super(message); }}class Product { private String id; private String name; private int stock; private double price; public Product(String id, String name, int stock, double price) { this.id = id; this.name = name; this.stock = stock; this.price = price; } public void purchase(int quantity) throws InvalidQuantityException, OutOfStockException { if (quantity <= 0) { throw new InvalidQuantityException( "Quantity must be positive. Got: " + quantity ); } if (quantity > stock) { throw new OutOfStockException(name, quantity, stock); } stock -= quantity; System.out.println("✓ Purchased " + quantity + " x " + name); System.out.println(" Total: $" + (price * quantity)); System.out.println(" Remaining stock: " + stock); } public String getName() { return name; }}public class ECommerceExceptions { public static void main(String[] args) { Product laptop = new Product("P001", "Laptop", 5, 999.99); // Test 1: Valid purchase try { System.out.println("=== Test 1: Valid Purchase ==="); laptop.purchase(2); } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } // Test 2: Out of stock try { System.out.println("\n=== Test 2: Out of Stock ==="); laptop.purchase(10); } catch (OutOfStockException e) { System.out.println("❌ " + e.getMessage()); System.out.println("Suggestion: Order " + e.getAvailableQuantity() + " units instead"); } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } // Test 3: Invalid quantity try { System.out.println("\n=== Test 3: Invalid Quantity ==="); laptop.purchase(-1); } catch (InvalidQuantityException e) { System.out.println("❌ " + e.getMessage()); } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } }}// Output:// === Test 1: Valid Purchase ===// ✓ Purchased 2 x Laptop// Total: $1999.98// Remaining stock: 3//// === Test 2: Out of Stock ===// ❌ Laptop is out of stock. Requested: 10, Available: 3// Suggestion: Order 3 units instead//// === Test 3: Invalid Quantity ===// ❌ Quantity must be positive. Got: -1⚡ Unchecked Custom Exceptions
// Extending RuntimeException makes it uncheckedclass InvalidConfigurationException extends RuntimeException { private String configKey; public InvalidConfigurationException(String configKey, String message) { super("Invalid configuration for '" + configKey + "': " + message); this.configKey = configKey; } public String getConfigKey() { return configKey; }}class ConfigManager { // No need for 'throws' clause - it's unchecked! public String getConfig(String key) { if (key == null || key.isEmpty()) { throw new InvalidConfigurationException( key, "Key cannot be null or empty" ); } // Simulate config lookup if (key.equals("database.url")) { return "jdbc:mysql://localhost:3306/mydb"; } throw new InvalidConfigurationException( key, "Configuration key not found" ); }}public class UncheckedExceptions { public static void main(String[] args) { ConfigManager config = new ConfigManager(); // No try-catch required (but recommended) String dbUrl = config.getConfig("database.url"); System.out.println("Database URL: " + dbUrl); // This will throw exception try { config.getConfig("invalid.key"); } catch (InvalidConfigurationException e) { System.out.println("\nError: " + e.getMessage()); System.out.println("Missing key: " + e.getConfigKey()); } }}// Output:// Database URL: jdbc:mysql://localhost:3306/mydb//// Error: Invalid configuration for 'invalid.key': Configuration key not found// Missing key: invalid.key// Key difference:// - Extends Exception = Checked (must declare with throws)// - Extends RuntimeException = Unchecked (optional handling)✅ Real-World Example: Validation Framework
// Base validation exceptionclass ValidationException extends Exception { private String fieldName; private Object invalidValue; public ValidationException(String fieldName, Object value, String reason) { super("Validation failed for '" + fieldName + "': " + reason + " (value: " + value + ")"); this.fieldName = fieldName; this.invalidValue = value; } public String getFieldName() { return fieldName; } public Object getInvalidValue() { return invalidValue; }}// Specific validation exceptionsclass TooShortException extends ValidationException { public TooShortException(String fieldName, String value, int minLength) { super(fieldName, value, "Must be at least " + minLength + " characters"); }}class TooLongException extends ValidationException { public TooLongException(String fieldName, String value, int maxLength) { super(fieldName, value, "Must not exceed " + maxLength + " characters"); }}class InvalidFormatException extends ValidationException { public InvalidFormatException(String fieldName, String value, String format) { super(fieldName, value, "Must match format: " + format); }}class UserValidator { public void validateUsername(String username) throws ValidationException { if (username == null) { throw new ValidationException("username", null, "Cannot be null"); } if (username.length() < 3) { throw new TooShortException("username", username, 3); } if (username.length() > 20) { throw new TooLongException("username", username, 20); } if (!username.matches("[a-zA-Z0-9_]+")) { throw new InvalidFormatException( "username", username, "letters, numbers, and underscore only" ); } } public void validatePassword(String password) throws ValidationException { if (password == null) { throw new ValidationException("password", null, "Cannot be null"); } if (password.length() < 8) { throw new TooShortException("password", "****", 8); } }}public class ValidationFramework { public static void main(String[] args) { UserValidator validator = new UserValidator(); testValidation(validator, "alice", "SecurePass123"); testValidation(validator, "ab", "SecurePass123"); testValidation(validator, "user@123", "SecurePass123"); testValidation(validator, "validuser", "123"); } public static void testValidation(UserValidator validator, String username, String password) { System.out.println("\n=== Validating User ==="); System.out.println("Username: " + username); System.out.println("Password: " + password); try { validator.validateUsername(username); validator.validatePassword(password); System.out.println("✓ Validation passed!"); } catch (TooShortException e) { System.out.println("❌ " + e.getMessage()); System.out.println(" Field: " + e.getFieldName()); } catch (TooLongException e) { System.out.println("❌ " + e.getMessage()); } catch (InvalidFormatException e) { System.out.println("❌ " + e.getMessage()); } catch (ValidationException e) { System.out.println("❌ " + e.getMessage()); } }}// Output:// === Validating User ===// Username: alice// Password: SecurePass123// ✓ Validation passed!//// === Validating User ===// Username: ab// Password: SecurePass123// ❌ Validation failed for 'username': Must be at least 3 characters (value: ab)// Field: username//// === Validating User ===// Username: user@123// Password: SecurePass123// ❌ Validation failed for 'username': Must match format: letters, numbers, and underscore only (value: user@123)//// === Validating User ===// Username: validuser// Password: 123// ❌ Validation failed for 'password': Must be at least 8 characters (value: ****)🔑 Key Concepts
Extending Exception
For checked exceptions that must be handled
class MyException extends Exception
Extending RuntimeException
For unchecked exceptions (optional handling)
class MyException extends RuntimeException
Custom Constructors
Add constructors for flexibility
MyException(String message, int errorCode)
Additional Fields
Store extra error context
private int errorCode; private String details;
✨ Best Practices
- ✓Name custom exceptions with 'Exception' suffix (e.g., InsufficientFundsException)
- ✓Extend Exception for checked, RuntimeException for unchecked
- ✓Provide multiple constructors for flexibility
- ✓Include meaningful default messages
- ✓Add fields to store error context (error codes, timestamps, etc.)
- ✓Override toString() if you need custom formatting
💼 Interview Tips
- •Know when to create custom exceptions vs using built-in ones
- •Understand the difference between checked and unchecked custom exceptions
- •Be able to explain the constructor chain with super()
- •Know common patterns: InvalidXException, XNotFoundException, etc.
- •Understand how custom exceptions integrate with try-catch
- •Be prepared to discuss exception class hierarchies