Custom Exception Creation in Java
Custom Exceptions in Java
Custom exceptions are user-defined exception classes that extend existing exception classes to create specific error conditions for your application domain. They provide more meaningful error messages and allow for more precise error handling compared to generic exceptions. Creating custom exceptions is essential for building robust, maintainable applications with clear error semantics.
Why Create Custom Exceptions?
Using custom exceptions offers several important advantages over relying solely on built-in Java exceptions:
Benefits of Custom Exceptions:
- Domain-Specific Context: Provide meaningful names that reflect your business logic
- Better Error Messages: Include specific information relevant to your application
- Precise Exception Handling: Allow different handling strategies for different error types
- API Clarity: Make your API's error conditions explicit and documented
- Debugging Aid: Easier to identify the source and nature of problems
- Logging and Monitoring: Enable better categorization of errors for analysis
// Poor approach - using generic exceptions
public class BadUserService {
public User findUser(String id) throws Exception {
if (id == null) {
throw new Exception("ID is null"); // Too generic
}
if (id.isEmpty()) {
throw new Exception("ID is empty"); // Too generic
}
if (!userExists(id)) {
throw new Exception("User not found"); // Unclear what happened
}
return loadUser(id);
}
private boolean userExists(String id) { return false; }
private User loadUser(String id) { return null; }
}
// Better approach - using custom exceptions
public class GoodUserService {
public User findUser(String id) throws InvalidUserIdException, UserNotFoundException {
if (id == null || id.isEmpty()) {
throw new InvalidUserIdException("User ID cannot be null or empty: " + id);
}
if (!userExists(id)) {
throw new UserNotFoundException("User not found with ID: " + id);
}
return loadUser(id);
}
private boolean userExists(String id) { return false; }
private User loadUser(String id) { return null; }
}
// Custom exception classes
class InvalidUserIdException extends Exception {
public InvalidUserIdException(String message) {
super(message);
}
}
class UserNotFoundException extends Exception {
public UserNotFoundException(String message) {
super(message);
}
}
class User {
private String id;
private String name;
// Constructor, getters, setters...
}
Creating Custom Checked Exceptions
Checked exceptions extend Exception
and must be declared in method signatures or handled with try-catch blocks:
import java.io.IOException;
import java.time.LocalDateTime;
public class CustomCheckedExceptions {
// Basic custom checked exception
public static class DatabaseConnectionException extends Exception {
public DatabaseConnectionException(String message) {
super(message);
}
public DatabaseConnectionException(String message, Throwable cause) {
super(message, cause);
}
}
// More sophisticated custom exception with additional data
public static class InsufficientFundsException extends Exception {
private final double requestedAmount;
private final double availableBalance;
private final String accountId;
public InsufficientFundsException(String accountId, double requestedAmount, double availableBalance) {
super(String.format("Insufficient funds in account %s. Requested: %.2f, Available: %.2f",
accountId, requestedAmount, availableBalance));
this.accountId = accountId;
this.requestedAmount = requestedAmount;
this.availableBalance = availableBalance;
}
public double getRequestedAmount() { return requestedAmount; }
public double getAvailableBalance() { return availableBalance; }
public String getAccountId() { return accountId; }
public double getShortfall() { return requestedAmount - availableBalance; }
}
// Exception with severity levels
public static class ValidationException extends Exception {
public enum Severity {
WARNING, ERROR, CRITICAL
}
private final Severity severity;
private final String fieldName;
private final Object invalidValue;
public ValidationException(String fieldName, Object invalidValue, String message, Severity severity) {
super(String.format("Validation failed for field '%s' with value '%s': %s",
fieldName, invalidValue, message));
this.fieldName = fieldName;
this.invalidValue = invalidValue;
this.severity = severity;
}
public Severity getSeverity() { return severity; }
public String getFieldName() { return fieldName; }
public Object getInvalidValue() { return invalidValue; }
}
// Exception hierarchy for different error types
public static abstract class PaymentException extends Exception {
private final String transactionId;
private final LocalDateTime timestamp;
public PaymentException(String transactionId, String message) {
super(message);
this.transactionId = transactionId;
this.timestamp = LocalDateTime.now();
}
public PaymentException(String transactionId, String message, Throwable cause) {
super(message, cause);
this.transactionId = transactionId;
this.timestamp = LocalDateTime.now();
}
public String getTransactionId() { return transactionId; }
public LocalDateTime getTimestamp() { return timestamp; }
}
public static class PaymentDeclinedException extends PaymentException {
private final String declineReason;
public PaymentDeclinedException(String transactionId, String declineReason) {
super(transactionId, "Payment declined: " + declineReason);
this.declineReason = declineReason;
}
public String getDeclineReason() { return declineReason; }
}
public static class PaymentTimeoutException extends PaymentException {
private final int timeoutSeconds;
public PaymentTimeoutException(String transactionId, int timeoutSeconds) {
super(transactionId, "Payment timed out after " + timeoutSeconds + " seconds");
this.timeoutSeconds = timeoutSeconds;
}
public int getTimeoutSeconds() { return timeoutSeconds; }
}
// Example service using custom exceptions
public static class BankingService {
public void withdraw(String accountId, double amount)
throws DatabaseConnectionException, InsufficientFundsException, ValidationException {
// Validate input
if (amount <= 0) {
throw new ValidationException("amount", amount, "Amount must be positive",
ValidationException.Severity.ERROR);
}
if (amount > 10000) {
throw new ValidationException("amount", amount, "Amount exceeds daily limit",
ValidationException.Severity.CRITICAL);
}
// Simulate database connection issue
if (Math.random() < 0.1) {
throw new DatabaseConnectionException("Unable to connect to account database");
}
// Check balance
double balance = getAccountBalance(accountId);
if (balance < amount) {
throw new InsufficientFundsException(accountId, amount, balance);
}
// Perform withdrawal
System.out.println("Withdrawal successful: " + amount + " from account " + accountId);
}
public void processPayment(String transactionId, double amount)
throws PaymentDeclinedException, PaymentTimeoutException {
// Simulate payment processing
double random = Math.random();
if (random < 0.2) {
throw new PaymentDeclinedException(transactionId, "Insufficient credit limit");
}
if (random < 0.3) {
throw new PaymentTimeoutException(transactionId, 30);
}
System.out.println("Payment processed successfully: Transaction " + transactionId);
}
private double getAccountBalance(String accountId) {
// Simulate account balance
return 1000.0 + Math.random() * 5000.0;
}
}
public static void demonstrateCheckedExceptions() {
BankingService service = new BankingService();
System.out.println("=== Testing Withdrawal ===");
try {
service.withdraw("ACC123", 500.0);
} catch (ValidationException e) {
System.err.println("Validation Error: " + e.getMessage());
System.err.println("Severity: " + e.getSeverity());
System.err.println("Field: " + e.getFieldName());
} catch (InsufficientFundsException e) {
System.err.println("Insufficient Funds: " + e.getMessage());
System.err.println("Shortfall: $" + String.format("%.2f", e.getShortfall()));
} catch (DatabaseConnectionException e) {
System.err.println("Database Error: " + e.getMessage());
}
System.out.println("\n=== Testing Payment ===");
try {
service.processPayment("TXN456", 1200.0);
} catch (PaymentDeclinedException e) {
System.err.println("Payment Declined: " + e.getMessage());
System.err.println("Transaction ID: " + e.getTransactionId());
System.err.println("Decline Reason: " + e.getDeclineReason());
} catch (PaymentTimeoutException e) {
System.err.println("Payment Timeout: " + e.getMessage());
System.err.println("Timeout Duration: " + e.getTimeoutSeconds() + " seconds");
}
}
public static void main(String[] args) {
demonstrateCheckedExceptions();
}
}
Creating Custom Unchecked Exceptions
Unchecked exceptions extend RuntimeException
and don't need to be declared in method signatures:
import java.util.*;
public class CustomUncheckedExceptions {
// Basic custom runtime exception
public static class ConfigurationException extends RuntimeException {
public ConfigurationException(String message) {
super(message);
}
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
// Business logic exception with context
public static class BusinessRuleViolationException extends RuntimeException {
private final String ruleName;
private final Map<String, Object> context;
public BusinessRuleViolationException(String ruleName, String message) {
super("Business rule violation [" + ruleName + "]: " + message);
this.ruleName = ruleName;
this.context = new HashMap<>();
}
public BusinessRuleViolationException addContext(String key, Object value) {
this.context.put(key, value);
return this;
}
public String getRuleName() { return ruleName; }
public Map<String, Object> getContext() { return Collections.unmodifiableMap(context); }
}
// Exception for invalid state
public static class InvalidOperationException extends RuntimeException {
private final String currentState;
private final String attemptedOperation;
public InvalidOperationException(String currentState, String attemptedOperation) {
super(String.format("Cannot perform '%s' operation in '%s' state",
attemptedOperation, currentState));
this.currentState = currentState;
this.attemptedOperation = attemptedOperation;
}
public String getCurrentState() { return currentState; }
public String getAttemptedOperation() { return attemptedOperation; }
}
// Resource-related exception
public static class ResourceExhaustedException extends RuntimeException {
private final String resourceType;
private final long maxCapacity;
private final long currentUsage;
public ResourceExhaustedException(String resourceType, long currentUsage, long maxCapacity) {
super(String.format("%s exhausted: %d/%d (%.1f%% used)",
resourceType, currentUsage, maxCapacity,
(currentUsage * 100.0) / maxCapacity));
this.resourceType = resourceType;
this.currentUsage = currentUsage;
this.maxCapacity = maxCapacity;
}
public String getResourceType() { return resourceType; }
public long getMaxCapacity() { return maxCapacity; }
public long getCurrentUsage() { return currentUsage; }
public double getUsagePercentage() { return (currentUsage * 100.0) / maxCapacity; }
}
// Exception hierarchy for different error categories
public static abstract class ServiceException extends RuntimeException {
private final String serviceId;
private final long timestamp;
public ServiceException(String serviceId, String message) {
super(message);
this.serviceId = serviceId;
this.timestamp = System.currentTimeMillis();
}
public ServiceException(String serviceId, String message, Throwable cause) {
super(message, cause);
this.serviceId = serviceId;
this.timestamp = System.currentTimeMillis();
}
public String getServiceId() { return serviceId; }
public long getTimestamp() { return timestamp; }
}
public static class ServiceUnavailableException extends ServiceException {
private final String reason;
private final long estimatedRecoveryTime;
public ServiceUnavailableException(String serviceId, String reason, long estimatedRecoveryTime) {
super(serviceId, String.format("Service '%s' unavailable: %s (estimated recovery: %d ms)",
serviceId, reason, estimatedRecoveryTime));
this.reason = reason;
this.estimatedRecoveryTime = estimatedRecoveryTime;
}
public String getReason() { return reason; }
public long getEstimatedRecoveryTime() { return estimatedRecoveryTime; }
}
public static class ServiceOverloadedException extends ServiceException {
private final int currentLoad;
private final int maxCapacity;
public ServiceOverloadedException(String serviceId, int currentLoad, int maxCapacity) {
super(serviceId, String.format("Service '%s' overloaded: %d/%d requests",
serviceId, currentLoad, maxCapacity));
this.currentLoad = currentLoad;
this.maxCapacity = maxCapacity;
}
public int getCurrentLoad() { return currentLoad; }
public int getMaxCapacity() { return maxCapacity; }
}
// Example classes using custom runtime exceptions
public static class OrderProcessor {
private enum State { CREATED, PROCESSING, COMPLETED, CANCELLED }
private State currentState = State.CREATED;
private int processedItems = 0;
private final int maxItems = 100;
public void startProcessing() {
if (currentState != State.CREATED) {
throw new InvalidOperationException(currentState.name(), "start processing");
}
currentState = State.PROCESSING;
System.out.println("Order processing started");
}
public void addItem() {
if (currentState != State.PROCESSING) {
throw new InvalidOperationException(currentState.name(), "add item");
}
if (processedItems >= maxItems) {
throw new ResourceExhaustedException("Order capacity", processedItems, maxItems);
}
processedItems++;
System.out.println("Item added. Total: " + processedItems);
}
public void completeOrder() {
if (currentState != State.PROCESSING) {
throw new InvalidOperationException(currentState.name(), "complete order");
}
if (processedItems == 0) {
throw new BusinessRuleViolationException("minimum-items", "Order must contain at least one item")
.addContext("currentItems", processedItems)
.addContext("minimumRequired", 1);
}
currentState = State.COMPLETED;
System.out.println("Order completed with " + processedItems + " items");
}
}
public static class ConfigurationService {
private final Map<String, String> config = new HashMap<>();
public ConfigurationService() {
// Simulate configuration loading
config.put("database.url", "jdbc:mysql://localhost:3306/mydb");
config.put("api.timeout", "5000");
}
public String getRequiredConfig(String key) {
String value = config.get(key);
if (value == null) {
throw new ConfigurationException("Required configuration key not found: " + key);
}
return value;
}
public int getIntConfig(String key) {
String value = getRequiredConfig(key);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new ConfigurationException("Configuration value is not a valid integer: " +
key + " = " + value, e);
}
}
}
public static void demonstrateRuntimeExceptions() {
System.out.println("=== Testing Order Processor ===");
OrderProcessor processor = new OrderProcessor();
try {
processor.startProcessing();
// Add some items
for (int i = 0; i < 5; i++) {
processor.addItem();
}
processor.completeOrder();
} catch (InvalidOperationException e) {
System.err.println("Invalid Operation: " + e.getMessage());
System.err.println("Current State: " + e.getCurrentState());
System.err.println("Attempted Operation: " + e.getAttemptedOperation());
} catch (ResourceExhaustedException e) {
System.err.println("Resource Exhausted: " + e.getMessage());
System.err.println("Usage: " + String.format("%.1f%%", e.getUsagePercentage()));
} catch (BusinessRuleViolationException e) {
System.err.println("Business Rule Violation: " + e.getMessage());
System.err.println("Rule: " + e.getRuleName());
System.err.println("Context: " + e.getContext());
}
System.out.println("\n=== Testing Configuration Service ===");
ConfigurationService configService = new ConfigurationService();
try {
String dbUrl = configService.getRequiredConfig("database.url");
System.out.println("Database URL: " + dbUrl);
int timeout = configService.getIntConfig("api.timeout");
System.out.println("API Timeout: " + timeout);
// This will throw an exception
String missing = configService.getRequiredConfig("missing.key");
} catch (ConfigurationException e) {
System.err.println("Configuration Error: " + e.getMessage());
if (e.getCause() != null) {
System.err.println("Caused by: " + e.getCause().getMessage());
}
}
}
public static void main(String[] args) {
demonstrateRuntimeExceptions();
}
}
Exception Hierarchies and Design Patterns
Well-designed exception hierarchies improve code organization and error handling:
import java.time.LocalDateTime;
import java.util.*;
public class ExceptionHierarchies {
// Base application exception
public static abstract class ApplicationException extends Exception {
private final String errorCode;
private final LocalDateTime timestamp;
private final Map<String, Object> details;
public ApplicationException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
this.details = new HashMap<>();
}
public ApplicationException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
this.details = new HashMap<>();
}
public String getErrorCode() { return errorCode; }
public LocalDateTime getTimestamp() { return timestamp; }
public Map<String, Object> getDetails() { return Collections.unmodifiableMap(details); }
public ApplicationException addDetail(String key, Object value) {
this.details.put(key, value);
return this;
}
public abstract String getCategory();
}
// Domain-specific exception categories
public static abstract class UserException extends ApplicationException {
public UserException(String errorCode, String message) {
super(errorCode, message);
}
public UserException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public String getCategory() { return "USER"; }
}
public static abstract class SecurityException extends ApplicationException {
public SecurityException(String errorCode, String message) {
super(errorCode, message);
}
public SecurityException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public String getCategory() { return "SECURITY"; }
}
public static abstract class SystemException extends ApplicationException {
public SystemException(String errorCode, String message) {
super(errorCode, message);
}
public SystemException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
@Override
public String getCategory() { return "SYSTEM"; }
}
// Specific user exceptions
public static class UserNotFoundException extends UserException {
public UserNotFoundException(String userId) {
super("USER_NOT_FOUND", "User not found with ID: " + userId);
addDetail("userId", userId);
}
}
public static class DuplicateUserException extends UserException {
public DuplicateUserException(String email) {
super("DUPLICATE_USER", "User already exists with email: " + email);
addDetail("email", email);
}
}
public static class InvalidUserDataException extends UserException {
private final List<String> validationErrors;
public InvalidUserDataException(List<String> validationErrors) {
super("INVALID_USER_DATA", "User data validation failed: " +
String.join(", ", validationErrors));
this.validationErrors = new ArrayList<>(validationErrors);
addDetail("validationErrors", validationErrors);
}
public List<String> getValidationErrors() {
return Collections.unmodifiableList(validationErrors);
}
}
// Specific security exceptions
public static class AuthenticationFailedException extends SecurityException {
public AuthenticationFailedException(String username) {
super("AUTH_FAILED", "Authentication failed for user: " + username);
addDetail("username", username);
addDetail("attemptTime", LocalDateTime.now());
}
}
public static class InsufficientPrivilegesException extends SecurityException {
private final String requiredRole;
private final String userRole;
public InsufficientPrivilegesException(String requiredRole, String userRole) {
super("INSUFFICIENT_PRIVILEGES",
String.format("Required role: %s, User role: %s", requiredRole, userRole));
this.requiredRole = requiredRole;
this.userRole = userRole;
addDetail("requiredRole", requiredRole);
addDetail("userRole", userRole);
}
public String getRequiredRole() { return requiredRole; }
public String getUserRole() { return userRole; }
}
// Specific system exceptions
public static class DatabaseConnectionException extends SystemException {
public DatabaseConnectionException(String databaseUrl, Throwable cause) {
super("DB_CONNECTION_FAILED", "Failed to connect to database: " + databaseUrl, cause);
addDetail("databaseUrl", databaseUrl);
}
}
public static class ExternalServiceException extends SystemException {
private final String serviceName;
private final int statusCode;
public ExternalServiceException(String serviceName, int statusCode, String message) {
super("EXTERNAL_SERVICE_ERROR",
String.format("External service '%s' error (HTTP %d): %s",
serviceName, statusCode, message));
this.serviceName = serviceName;
this.statusCode = statusCode;
addDetail("serviceName", serviceName);
addDetail("statusCode", statusCode);
}
public String getServiceName() { return serviceName; }
public int getStatusCode() { return statusCode; }
}
// Exception factory for consistent creation
public static class ExceptionFactory {
public static UserNotFoundException userNotFound(String userId) {
return new UserNotFoundException(userId);
}
public static DuplicateUserException duplicateUser(String email) {
return new DuplicateUserException(email);
}
public static InvalidUserDataException invalidUserData(String... errors) {
return new InvalidUserDataException(Arrays.asList(errors));
}
public static AuthenticationFailedException authenticationFailed(String username) {
return new AuthenticationFailedException(username);
}
public static InsufficientPrivilegesException insufficientPrivileges(String required, String actual) {
return new InsufficientPrivilegesException(required, actual);
}
public static DatabaseConnectionException databaseConnectionFailed(String url, Throwable cause) {
return new DatabaseConnectionException(url, cause);
}
public static ExternalServiceException externalServiceError(String service, int status, String message) {
return new ExternalServiceException(service, status, message);
}
}
// Exception handler that provides consistent error processing
public static class ExceptionHandler {
public static void handleException(ApplicationException e) {
System.err.println("=== Exception Details ===");
System.err.println("Category: " + e.getCategory());
System.err.println("Error Code: " + e.getErrorCode());
System.err.println("Message: " + e.getMessage());
System.err.println("Timestamp: " + e.getTimestamp());
if (!e.getDetails().isEmpty()) {
System.err.println("Details:");
e.getDetails().forEach((key, value) ->
System.err.println(" " + key + ": " + value));
}
if (e.getCause() != null) {
System.err.println("Caused by: " + e.getCause().getMessage());
}
// Log based on category
switch (e.getCategory()) {
case "SECURITY":
System.err.println("🔒 Security alert logged");
break;
case "SYSTEM":
System.err.println("⚠️ System error logged for monitoring");
break;
case "USER":
System.err.println("👤 User error logged for analytics");
break;
}
System.err.println();
}
}
// Example service demonstrating exception usage
public static class UserService {
private final Map<String, String> users = new HashMap<>();
public UserService() {
users.put("user1", "[email protected]");
users.put("user2", "[email protected]");
}
public String getUser(String userId) throws UserNotFoundException {
if (!users.containsKey(userId)) {
throw ExceptionFactory.userNotFound(userId);
}
return users.get(userId);
}
public void createUser(String userId, String email)
throws DuplicateUserException, InvalidUserDataException {
// Validate input
List<String> errors = new ArrayList<>();
if (userId == null || userId.trim().isEmpty()) {
errors.add("User ID cannot be empty");
}
if (email == null || !email.contains("@")) {
errors.add("Invalid email format");
}
if (!errors.isEmpty()) {
throw ExceptionFactory.invalidUserData(errors.toArray(new String[0]));
}
// Check for duplicates
if (users.containsKey(userId)) {
throw ExceptionFactory.duplicateUser(email);
}
users.put(userId, email);
System.out.println("User created: " + userId + " -> " + email);
}
public void authenticateUser(String userId, String password)
throws UserNotFoundException, AuthenticationFailedException {
if (!users.containsKey(userId)) {
throw ExceptionFactory.userNotFound(userId);
}
// Simulate authentication
if (!"password123".equals(password)) {
throw ExceptionFactory.authenticationFailed(userId);
}
System.out.println("User authenticated: " + userId);
}
}
public static void demonstrateExceptionHierarchy() {
UserService userService = new UserService();
ExceptionHandler handler = new ExceptionHandler();
// Test various exception scenarios
try {
userService.getUser("nonexistent");
} catch (ApplicationException e) {
handler.handleException(e);
}
try {
userService.createUser("", "invalid-email");
} catch (ApplicationException e) {
handler.handleException(e);
}
try {
userService.createUser("user1", "[email protected]");
} catch (ApplicationException e) {
handler.handleException(e);
}
try {
userService.authenticateUser("user1", "wrongpassword");
} catch (ApplicationException e) {
handler.handleException(e);
}
}
public static void main(String[] args) {
demonstrateExceptionHierarchy();
}
}
Best Practices for Custom Exceptions
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Logger;
public class ExceptionBestPractices {
private static final Logger logger = Logger.getLogger(ExceptionBestPractices.class.getName());
// 1. Follow naming conventions
public static class GoodNaming {
// GOOD: Clear, descriptive names ending with "Exception"
public static class PaymentProcessingException extends Exception {}
public static class InvalidCreditCardException extends Exception {}
public static class DatabaseConnectionException extends Exception {}
// AVOID: Vague or non-descriptive names
// public static class ErrorException extends Exception {} // Too generic
// public static class Problem extends Exception {} // Doesn't follow convention
}
// 2. Provide multiple constructors
public static class WellDesignedException extends Exception {
private final String errorCode;
// Default constructor with code
public WellDesignedException(String errorCode) {
this.errorCode = errorCode;
}
// Constructor with message
public WellDesignedException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// Constructor with cause
public WellDesignedException(String errorCode, Throwable cause) {
super(cause);
this.errorCode = errorCode;
}
// Constructor with message and cause
public WellDesignedException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// 3. Include relevant context information
public static class ContextualException extends RuntimeException {
private final String operation;
private final Object[] parameters;
private final String context;
public ContextualException(String operation, String context, Object... parameters) {
super(String.format("Operation '%s' failed in context '%s' with parameters: %s",
operation, context, java.util.Arrays.toString(parameters)));
this.operation = operation;
this.context = context;
this.parameters = parameters.clone();
}
public String getOperation() { return operation; }
public Object[] getParameters() { return parameters.clone(); }
public String getContext() { return context; }
}
// 4. Make exceptions immutable and thread-safe
public static final class ImmutableException extends Exception {
private final String errorId;
private final long timestamp;
private final java.util.Map<String, Object> metadata;
public ImmutableException(String errorId, String message,
java.util.Map<String, Object> metadata) {
super(message);
this.errorId = errorId;
this.timestamp = System.currentTimeMillis();
this.metadata = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(metadata));
}
public String getErrorId() { return errorId; }
public long getTimestamp() { return timestamp; }
public java.util.Map<String, Object> getMetadata() { return metadata; }
}
// 5. Provide utility methods for common operations
public static class UtilityException extends Exception {
public UtilityException(String message) {
super(message);
}
public UtilityException(String message, Throwable cause) {
super(message, cause);
}
// Get the full stack trace as a string
public String getStackTraceAsString() {
StringWriter sw = new StringWriter();
this.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
// Get a concise error summary
public String getErrorSummary() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName()).append(": ").append(getMessage());
Throwable cause = getCause();
while (cause != null) {
sb.append(" → ").append(cause.getClass().getSimpleName())
.append(": ").append(cause.getMessage());
cause = cause.getCause();
}
return sb.toString();
}
// Check if this exception was caused by a specific type
public boolean wasCausedBy(Class<? extends Throwable> exceptionType) {
Throwable cause = getCause();
while (cause != null) {
if (exceptionType.isInstance(cause)) {
return true;
}
cause = cause.getCause();
}
return false;
}
}
// 6. Use builder pattern for complex exceptions
public static class ComplexException extends RuntimeException {
private final String errorCode;
private final int severity;
private final java.util.Map<String, Object> context;
private final java.util.List<String> suggestions;
private ComplexException(Builder builder) {
super(builder.message, builder.cause);
this.errorCode = builder.errorCode;
this.severity = builder.severity;
this.context = java.util.Collections.unmodifiableMap(builder.context);
this.suggestions = java.util.Collections.unmodifiableList(builder.suggestions);
}
public String getErrorCode() { return errorCode; }
public int getSeverity() { return severity; }
public java.util.Map<String, Object> getContext() { return context; }
public java.util.List<String> getSuggestions() { return suggestions; }
public static class Builder {
private String message;
private String errorCode;
private Throwable cause;
private int severity = 1;
private final java.util.Map<String, Object> context = new java.util.HashMap<>();
private final java.util.List<String> suggestions = new java.util.ArrayList<>();
public Builder message(String message) {
this.message = message;
return this;
}
public Builder errorCode(String errorCode) {
this.errorCode = errorCode;
return this;
}
public Builder cause(Throwable cause) {
this.cause = cause;
return this;
}
public Builder severity(int severity) {
this.severity = severity;
return this;
}
public Builder addContext(String key, Object value) {
this.context.put(key, value);
return this;
}
public Builder addSuggestion(String suggestion) {
this.suggestions.add(suggestion);
return this;
}
public ComplexException build() {
if (message == null) {
throw new IllegalStateException("Message is required");
}
if (errorCode == null) {
throw new IllegalStateException("Error code is required");
}
return new ComplexException(this);
}
}
}
// 7. Logging integration best practices
public static class LoggingAwareException extends RuntimeException {
private final boolean shouldLog;
private final java.util.logging.Level logLevel;
public LoggingAwareException(String message, boolean shouldLog,
java.util.logging.Level logLevel) {
super(message);
this.shouldLog = shouldLog;
this.logLevel = logLevel;
if (shouldLog) {
logger.log(logLevel, "Exception created: " + message, this);
}
}
public boolean shouldLog() { return shouldLog; }
public java.util.logging.Level getLogLevel() { return logLevel; }
}
// 8. Recovery suggestions
public static class RecoverableException extends Exception {
private final java.util.List<String> recoverySuggestions;
private final boolean isRetryable;
public RecoverableException(String message, boolean isRetryable, String... suggestions) {
super(message);
this.isRetryable = isRetryable;
this.recoverySuggestions = java.util.Arrays.asList(suggestions);
}
public boolean isRetryable() { return isRetryable; }
public java.util.List<String> getRecoverySuggestions() {
return java.util.Collections.unmodifiableList(recoverySuggestions);
}
public void printRecoverySuggestions() {
System.err.println("Recovery suggestions:");
for (int i = 0; i < recoverySuggestions.size(); i++) {
System.err.println(" " + (i + 1) + ". " + recoverySuggestions.get(i));
}
if (isRetryable) {
System.err.println(" • This operation can be retried");
}
}
}
public static void demonstrateBestPractices() {
System.out.println("=== Exception Best Practices Demo ===");
// Complex exception with builder
try {
throw new ComplexException.Builder()
.message("Database operation failed")
.errorCode("DB_001")
.severity(3)
.addContext("table", "users")
.addContext("operation", "INSERT")
.addContext("rowCount", 1)
.addSuggestion("Check database connectivity")
.addSuggestion("Verify table permissions")
.addSuggestion("Review SQL syntax")
.build();
} catch (ComplexException e) {
System.err.println("Complex Exception:");
System.err.println(" Code: " + e.getErrorCode());
System.err.println(" Severity: " + e.getSeverity());
System.err.println(" Context: " + e.getContext());
System.err.println(" Suggestions: " + e.getSuggestions());
}
// Recoverable exception
try {
throw new RecoverableException(
"Network timeout occurred",
true,
"Check network connectivity",
"Increase timeout value",
"Try again later"
);
} catch (RecoverableException e) {
System.err.println("\nRecoverable Exception: " + e.getMessage());
e.printRecoverySuggestions();
}
// Utility exception
try {
throw new UtilityException("Demonstration error",
new RuntimeException("Root cause"));
} catch (UtilityException e) {
System.err.println("\nUtility Exception Summary: " + e.getErrorSummary());
System.err.println("Caused by RuntimeException: " +
e.wasCausedBy(RuntimeException.class));
}
}
public static void main(String[] args) {
demonstrateBestPractices();
}
}
Summary
Custom exceptions are essential for building robust Java applications:
Key Benefits:
- Domain-Specific Semantics: Meaningful names and context for business logic
- Better Error Handling: Precise catch blocks for different error types
- Enhanced Debugging: Rich context information for troubleshooting
- API Clarity: Explicit error conditions in method signatures
- Improved Monitoring: Categorized error tracking and analytics
Design Principles:
- Meaningful Names: Use descriptive names ending with "Exception"
- Appropriate Inheritance: Extend Exception (checked) or RuntimeException (unchecked)
- Rich Context: Include relevant data and error codes
- Immutability: Make exception objects immutable and thread-safe
- Multiple Constructors: Support various creation patterns
Exception Hierarchies:
- Base Classes: Common functionality and metadata
- Category Classes: Group related exceptions (Security, System, User)
- Specific Classes: Concrete exceptions for specific error conditions
- Factory Methods: Consistent exception creation
Best Practices:
- Provide Context: Include relevant data and error codes
- Use Builders: For complex exceptions with many properties
- Include Recovery Info: Suggestions and retry indicators
- Logging Integration: Appropriate logging levels and messages
- Documentation: Clear javadoc with usage examples
Common Patterns:
- Wrapper Exceptions: Convert checked to unchecked exceptions
- Chained Exceptions: Preserve root cause information
- Retry Logic: Include retry indicators and suggestions
- Error Codes: Systematic error identification
Custom exceptions transform generic error conditions into meaningful, actionable information that improves both developer experience and application reliability.