Custom Exceptions in Java

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

  1. Better Error Handling - More specific exception types
  2. Improved Debugging - Richer context and information
  3. Cleaner Code - More meaningful error reporting
  4. Domain-Specific - Exceptions that match your business domain
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper