Custom exceptions allow you to create application-specific exception types that provide more meaningful error information and better error handling in your applications.
1. Basic Custom Exception
Checked Exception
// Custom checked exception class InsufficientFundsException extends Exception { private double amount; private double balance; public InsufficientFundsException(double amount, double balance) { super("Insufficient funds: Attempted to withdraw " + amount + " but balance is " + balance); this.amount = amount; this.balance = balance; } public double getAmount() { return amount; } public double getBalance() { return balance; } public double getRequired() { return amount - balance; } } // Usage class BankAccount { private double balance; public BankAccount(double balance) { this.balance = balance; } public void withdraw(double amount) throws InsufficientFundsException { if (amount > balance) { throw new InsufficientFundsException(amount, balance); } balance -= amount; } public double getBalance() { return balance; } } public class CheckedExceptionExample { public static void main(String[] args) { BankAccount account = new BankAccount(1000); try { account.withdraw(1500); } catch (InsufficientFundsException e) { System.out.println("Error: " + e.getMessage()); System.out.println("Attempted: $" + e.getAmount()); System.out.println("Available: $" + e.getBalance()); System.out.println("Required: $" + e.getRequired()); } } } Unchecked Exception
// Custom unchecked exception class InvalidAgeException extends RuntimeException { private int age; private String requirement; public InvalidAgeException(int age, String requirement) { super("Invalid age: " + age + ". " + requirement); this.age = age; this.requirement = requirement; } public InvalidAgeException(int age, String requirement, Throwable cause) { super("Invalid age: " + age + ". " + requirement, cause); this.age = age; this.requirement = requirement; } public int getAge() { return age; } public String getRequirement() { return requirement; } } // Usage class AgeValidator { public static void validateAge(int age) { if (age < 0) { throw new InvalidAgeException(age, "Age cannot be negative"); } if (age < 18) { throw new InvalidAgeException(age, "Must be 18 or older"); } if (age > 120) { throw new InvalidAgeException(age, "Age seems unrealistic"); } System.out.println("Age " + age + " is valid"); } } public class UncheckedExceptionExample { public static void main(String[] args) { try { AgeValidator.validateAge(15); } catch (InvalidAgeException e) { System.out.println("Validation failed: " + e.getMessage()); System.out.println("Provided age: " + e.getAge()); System.out.println("Requirement: " + e.getRequirement()); } try { AgeValidator.validateAge(25); } catch (InvalidAgeException e) { System.out.println("Validation failed: " + e.getMessage()); } } } 2. Exception Hierarchy and Best Practices
Creating an Exception Hierarchy
// Base custom exception abstract class PaymentException extends Exception { private String transactionId; private double amount; public PaymentException(String message, String transactionId, double amount) { super(message); this.transactionId = transactionId; this.amount = amount; } public PaymentException(String message, String transactionId, double amount, Throwable cause) { super(message, cause); this.transactionId = transactionId; this.amount = amount; } public String getTransactionId() { return transactionId; } public double getAmount() { return amount; } public abstract String getErrorCode(); } // Specific payment exceptions class InsufficientBalanceException extends PaymentException { public InsufficientBalanceException(String transactionId, double amount, double availableBalance) { super("Insufficient balance for transaction " + transactionId + ". Required: " + amount + ", Available: " + availableBalance, transactionId, amount); } @Override public String getErrorCode() { return "PAYMENT_001"; } } class InvalidCardException extends PaymentException { private String cardNumber; public InvalidCardException(String transactionId, double amount, String cardNumber) { super("Invalid card number: " + maskCardNumber(cardNumber), transactionId, amount); this.cardNumber = cardNumber; } public InvalidCardException(String transactionId, double amount, String cardNumber, Throwable cause) { super("Invalid card number: " + maskCardNumber(cardNumber), transactionId, amount, cause); this.cardNumber = cardNumber; } @Override public String getErrorCode() { return "PAYMENT_002"; } public String getCardNumber() { return cardNumber; } private static String maskCardNumber(String cardNumber) { if (cardNumber == null || cardNumber.length() < 4) { return "****"; } return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4); } } class NetworkException extends PaymentException { public NetworkException(String transactionId, double amount, Throwable cause) { super("Network error during transaction " + transactionId, transactionId, amount, cause); } @Override public String getErrorCode() { return "PAYMENT_003"; } } // Payment processor using the exception hierarchy class PaymentProcessor { public void processPayment(String transactionId, double amount, String cardNumber) throws PaymentException { // Simulate various error conditions if (cardNumber == null || cardNumber.length() != 16) { throw new InvalidCardException(transactionId, amount, cardNumber); } if (amount > 1000) { // Simulate balance check throw new InsufficientBalanceException(transactionId, amount, 1000); } if (Math.random() < 0.1) { // 10% chance of network failure throw new NetworkException(transactionId, amount, new RuntimeException("Connection timeout")); } System.out.println("Payment processed successfully: " + transactionId); } } public class ExceptionHierarchyExample { public static void main(String[] args) { PaymentProcessor processor = new PaymentProcessor(); String[] transactions = { "TXN001", "TXN002", "TXN003" }; for (String transactionId : transactions) { try { processor.processPayment(transactionId, 1500, "1234567890123456"); } catch (PaymentException e) { handlePaymentError(e); } } } private static void handlePaymentError(PaymentException e) { System.out.println("\n=== Payment Error ==="); System.out.println("Error Code: " + e.getErrorCode()); System.out.println("Transaction: " + e.getTransactionId()); System.out.println("Amount: $" + e.getAmount()); System.out.println("Message: " + e.getMessage()); // Handle specific exception types if (e instanceof InsufficientBalanceException) { System.out.println("Error Type: Insufficient Balance"); // Suggest alternative payment methods } else if (e instanceof InvalidCardException) { InvalidCardException ice = (InvalidCardException) e; System.out.println("Error Type: Invalid Card"); System.out.println("Card: " + ice.getCardNumber()); // Prompt for card re-entry } else if (e instanceof NetworkException) { System.out.println("Error Type: Network Issue"); // Retry logic or offline processing if (e.getCause() != null) { System.out.println("Root cause: " + e.getCause().getMessage()); } } } } 3. Advanced Custom Exception Features
Exception with Resource Management
import java.time.LocalDateTime; import java.util.UUID; class DatabaseException extends Exception { private final String errorId; private final LocalDateTime timestamp; private final String sqlState; private final String databaseUrl; public DatabaseException(String message, String sqlState, String databaseUrl) { super(message); this.errorId = UUID.randomUUID().toString(); this.timestamp = LocalDateTime.now(); this.sqlState = sqlState; this.databaseUrl = databaseUrl; } public DatabaseException(String message, String sqlState, String databaseUrl, Throwable cause) { super(message, cause); this.errorId = UUID.randomUUID().toString(); this.timestamp = LocalDateTime.now(); this.sqlState = sqlState; this.databaseUrl = databaseUrl; } public String getErrorId() { return errorId; } public LocalDateTime getTimestamp() { return timestamp; } public String getSqlState() { return sqlState; } public String getDatabaseUrl() { return databaseUrl; } @Override public String toString() { return String.format( "DatabaseException{errorId='%s', timestamp=%s, sqlState='%s', databaseUrl='%s', message='%s'}", errorId, timestamp, sqlState, databaseUrl, getMessage() ); } } // Simulated database operations class DatabaseConnection { public void connect() throws DatabaseException { // Simulate connection failure if (Math.random() < 0.3) { throw new DatabaseException( "Connection refused", "08001", "jdbc:mysql://localhost:3306/mydb", new RuntimeException("Connection timeout") ); } System.out.println("Connected to database"); } public void executeQuery(String query) throws DatabaseException { // Simulate query failure if (query.toLowerCase().contains("drop")) { throw new DatabaseException( "Syntax error in SQL statement", "42000", "jdbc:mysql://localhost:3306/mydb" ); } System.out.println("Query executed: " + query); } } public class AdvancedExceptionExample { public static void main(String[] args) { DatabaseConnection db = new DatabaseConnection(); try { db.connect(); db.executeQuery("DROP TABLE users"); // This will fail } catch (DatabaseException e) { System.out.println("Database Error Details:"); System.out.println("Error ID: " + e.getErrorId()); System.out.println("Timestamp: " + e.getTimestamp()); System.out.println("SQL State: " + e.getSqlState()); System.out.println("Database: " + e.getDatabaseUrl()); System.out.println("Message: " + e.getMessage()); if (e.getCause() != null) { System.out.println("Root Cause: " + e.getCause().getMessage()); } // Log the complete exception System.out.println("\nComplete exception toString():"); System.out.println(e); } } } 4. Custom Exceptions with Internationalization
import java.util.ResourceBundle; import java.util.Locale; // Internationalized exception class LocalizedException extends Exception { private final String errorKey; private final Object[] parameters; private static ResourceBundle messages; static { // Load the resource bundle for error messages messages = ResourceBundle.getBundle("ErrorMessages", Locale.getDefault()); } public LocalizedException(String errorKey, Object... parameters) { super(formatMessage(errorKey, parameters)); this.errorKey = errorKey; this.parameters = parameters; } public LocalizedException(String errorKey, Throwable cause, Object... parameters) { super(formatMessage(errorKey, parameters), cause); this.errorKey = errorKey; this.parameters = parameters; } public String getErrorKey() { return errorKey; } public Object[] getParameters() { return parameters; } public String getLocalizedMessage(Locale locale) { ResourceBundle localizedMessages = ResourceBundle.getBundle("ErrorMessages", locale); return formatMessage(localizedMessages, errorKey, parameters); } private static String formatMessage(String key, Object... params) { return formatMessage(messages, key, params); } private static String formatMessage(ResourceBundle bundle, String key, Object... params) { try { String pattern = bundle.getString(key); return String.format(pattern, params); } catch (Exception e) { return "Error: " + key; // Fallback message } } } // Usage example class UserRegistration { public void registerUser(String username, int age, String email) throws LocalizedException { if (username == null || username.length() < 3) { throw new LocalizedException("error.username.invalid", username); } if (age < 18) { throw new LocalizedException("error.age.underage", age, 18); } if (email == null || !email.contains("@")) { throw new LocalizedException("error.email.invalid", email); } // Check for duplicate username (simulated) if ("admin".equals(username)) { throw new LocalizedException("error.username.taken", username); } System.out.println("User registered successfully: " + username); } } public class InternationalizedExceptionExample { public static void main(String[] args) { // Create a properties file named "ErrorMessages.properties" with: // error.username.invalid=Username '%s' is invalid. Must be at least 3 characters. // error.age.underage=Age %d is invalid. Must be at least %d years old. // error.email.invalid=Email address '%s' is invalid. // error.username.taken=Username '%s' is already taken. UserRegistration registration = new UserRegistration(); // Test different error scenarios testRegistration(registration, "ab", 20, "[email protected]"); // Short username testRegistration(registration, "john", 16, "[email protected]"); // Underage testRegistration(registration, "admin", 25, "[email protected]"); // Taken username testRegistration(registration, "validuser", 25, "invalid-email"); // Invalid email } private static void testRegistration(UserRegistration registration, String username, int age, String email) { try { registration.registerUser(username, age, email); } catch (LocalizedException e) { System.out.println("Registration failed:"); System.out.println(" Error Key: " + e.getErrorKey()); System.out.println(" Message: " + e.getMessage()); System.out.println(" Parameters: " + java.util.Arrays.toString(e.getParameters())); System.out.println(); } } } 5. Custom Exceptions for Validation Framework
import java.util.ArrayList; import java.util.List; // Validation exception that can accumulate multiple errors class ValidationException extends Exception { private final List<ValidationError> errors; public ValidationException(String message) { super(message); this.errors = new ArrayList<>(); } public ValidationException(List<ValidationError> errors) { super("Validation failed with " + errors.size() + " error(s)"); this.errors = new ArrayList<>(errors); } public void addError(ValidationError error) { errors.add(error); } public List<ValidationError> getErrors() { return new ArrayList<>(errors); } public boolean hasErrors() { return !errors.isEmpty(); } public int getErrorCount() { return errors.size(); } @Override public String getMessage() { if (errors.isEmpty()) { return super.getMessage(); } StringBuilder sb = new StringBuilder(); sb.append("Validation failed with ").append(errors.size()).append(" error(s):\n"); for (int i = 0; i < errors.size(); i++) { sb.append(i + 1).append(". ").append(errors.get(i).getMessage()).append("\n"); } return sb.toString(); } } class ValidationError { private final String field; private final String code; private final String message; private final Object rejectedValue; public ValidationError(String field, String code, String message, Object rejectedValue) { this.field = field; this.code = code; this.message = message; this.rejectedValue = rejectedValue; } // Getters public String getField() { return field; } public String getCode() { return code; } public String getMessage() { return message; } public Object getRejectedValue() { return rejectedValue; } @Override public String toString() { return String.format("ValidationError{field='%s', code='%s', message='%s', rejectedValue=%s}", field, code, message, rejectedValue); } } // Validator class class UserValidator { public void validateUser(User user) throws ValidationException { List<ValidationError> errors = new ArrayList<>(); // Validate username if (user.getUsername() == null || user.getUsername().trim().isEmpty()) { errors.add(new ValidationError("username", "REQUIRED", "Username is required", user.getUsername())); } else if (user.getUsername().length() < 3) { errors.add(new ValidationError("username", "MIN_LENGTH", "Username must be at least 3 characters", user.getUsername())); } else if (user.getUsername().length() > 20) { errors.add(new ValidationError("username", "MAX_LENGTH", "Username cannot exceed 20 characters", user.getUsername())); } // Validate email if (user.getEmail() == null || user.getEmail().trim().isEmpty()) { errors.add(new ValidationError("email", "REQUIRED", "Email is required", user.getEmail())); } else if (!user.getEmail().contains("@")) { errors.add(new ValidationError("email", "INVALID_FORMAT", "Email must contain @ symbol", user.getEmail())); } // Validate age if (user.getAge() < 0) { errors.add(new ValidationError("age", "INVALID_RANGE", "Age cannot be negative", user.getAge())); } else if (user.getAge() < 18) { errors.add(new ValidationError("age", "MIN_AGE", "Must be at least 18 years old", user.getAge())); } else if (user.getAge() > 120) { errors.add(new ValidationError("age", "MAX_AGE", "Age seems unrealistic", user.getAge())); } if (!errors.isEmpty()) { throw new ValidationException(errors); } } } class User { private String username; private String email; private int age; public User(String username, String email, int age) { this.username = username; this.email = email; this.age = age; } // Getters and setters public String getUsername() { return username; } public String getEmail() { return email; } public int getAge() { return age; } } public class ValidationExceptionExample { public static void main(String[] args) { UserValidator validator = new UserValidator(); // Test with invalid user User invalidUser = new User("ab", "invalid-email", 15); try { validator.validateUser(invalidUser); System.out.println("User is valid!"); } catch (ValidationException e) { System.out.println("Validation failed!"); System.out.println("Total errors: " + e.getErrorCount()); System.out.println("\nDetailed errors:"); for (ValidationError error : e.getErrors()) { System.out.println(" - Field: " + error.getField()); System.out.println(" Code: " + error.getCode()); System.out.println(" Message: " + error.getMessage()); System.out.println(" Rejected Value: " + error.getRejectedValue()); System.out.println(); } } // Test with valid user User validUser = new User("john_doe", "[email protected]", 25); try { validator.validateUser(validUser); System.out.println("User is valid!"); } catch (ValidationException e) { System.out.println("Validation failed: " + e.getMessage()); } } } 6. Best Practices for Custom Exceptions
1. Meaningful Naming
// Good - clearly indicates what went wrong class AccountLockedException extends SecurityException {} class OrderNotFoundException extends RuntimeException {} class InvalidConfigurationException extends Exception {} // Avoid - vague names class MyException extends Exception {} // Too generic class ErrorException extends Exception {} // Redundant 2. Provide Useful Context
class FileProcessingException extends Exception { private final String fileName; private final long fileSize; private final String operation; public FileProcessingException(String message, String fileName, long fileSize, String operation) { super(message); this.fileName = fileName; this.fileSize = fileSize; this.operation = operation; } public FileProcessingException(String message, String fileName, long fileSize, String operation, Throwable cause) { super(message, cause); this.fileName = fileName; this.fileSize = fileSize; this.operation = operation; } // Getters... } 3. Choose Checked vs Unchecked Appropriately
// Use checked exceptions for recoverable conditions class DatabaseConnectionException extends Exception {} // Caller can retry // Use unchecked exceptions for programming errors class InvalidArgumentException extends IllegalArgumentException {} // Bug in code // Use unchecked exceptions for unrecoverable conditions class SystemConfigurationException extends RuntimeException {} // Cannot proceed 4. Implement Proper Constructors
class ProperCustomException extends Exception { public ProperCustomException() { super(); } public ProperCustomException(String message) { super(message); } public ProperCustomException(String message, Throwable cause) { super(message, cause); } public ProperCustomException(Throwable cause) { super(cause); } } Key Benefits of Custom Exceptions
- Better Error Handling - More specific exception types
- Improved Debugging - Richer context and information
- Cleaner Code - More meaningful error reporting
- Domain-Specific - Exceptions that match your business domain
- Consistent Error Reporting - Standardized error handling across application
Custom exceptions make your code more robust, maintainable, and user-friendly by providing clear, actionable error information.