Custom Exceptions
Overview
Java's built-in exceptions (like IllegalArgumentException and IOException) are generic. In a real production application, you need exceptions that carry domain-specific meaning. When a user attempts to withdraw more money than their balance, throwing a generic RuntimeException is unhelpful. Throwing a InsufficientFundsException immediately communicates the exact business rule that was violated.
Custom Exceptions are created by extending either Exception (to create a Checked Exception that callers must handle) or RuntimeException (to create an Unchecked Exception). The best practice is to accept a message string and a cause Throwable in the constructors, forwarding both to the parent using super(). This ensures your custom exception integrates seamlessly with Java's standard exception chaining and logging infrastructure.
Well-designed custom exception hierarchies make a codebase dramatically easier to maintain. Instead of checking error codes in returned strings, callers can catch specific exception types and respond accordingly.
Syntax
// 1. Define the Custom Exception
// Extends RuntimeException → Unchecked, no 'throws' declaration needed
public class InsufficientFundsException extends RuntimeException {
private double shortfall; // Domain-specific data!
public InsufficientFundsException(double shortfall) {
super("Insufficient funds. You need $" + shortfall + " more.");
this.shortfall = shortfall;
}
// Custom getter for structured error responses
public double getShortfall() {
return shortfall;
}
}
// 2. Use the Custom Exception in Business Logic
public class BankAccount {
private double balance = 100.00;
public void withdraw(double amount) {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
}
// 3. Catch the Specific Business Exception
public class Main {
public static void main(String[] args) {
BankAccount acc = new BankAccount();
try {
acc.withdraw(250.00);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("You are short: $" + e.getShortfall());
}
}
}Common Pitfalls
- Not calling
super(message, cause)in the constructor. If you create a custom exception but pass nothing to super, the parent's message and cause chain are lost, makinge.getMessage()returnnulland log files completely useless. - Creating too many overly fine-grained exceptions. Creating
UserEmailInvalidException,UserPasswordTooShortException, andUserAgeUnderMinimumExceptionas three completely separate classes adds enormous boilerplate. Group related validations under oneUserValidationExceptionthat carries a specific error code field instead. - Extending the wrong base class. If you extend
Exception(Checked), every method in your codebase that calls anything throwing your exception must declarethrows YourException. This cascades through the entire call stack. ExtendRuntimeExceptionunless you have a very explicit reason to force compile-time handling.
Interview Questions
Exception vs RuntimeException for a Custom Exception?Extend Exception (Checked) when the failure is a foreseeable, recoverable business condition that the caller should always explicitly handle — like InsufficientFundsException. Extend RuntimeException (Unchecked) for programming errors or violations of contracts that represent bugs, not expected conditions — like InvalidConfigurationException.
cause parameter to your custom exception's constructor?To support Exception Chaining. When you catch a lower-level exception (like SQLException) and throw your own custom one, passing the original as the cause (super(message, cause)) preserves the full diagnostic chain. Without it, the original root cause is permanently hidden from logs and stack traces, making debugging exponentially harder.
Real-World Example
Spring Boot APIs use custom exceptions combined with @ControllerAdvice (a global exception handler) to automatically translate business exceptions into clean HTTP error responses, without any repetitive try-catch code in each controller.
// Custom Exception
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String id) {
super("Resource not found: " + id);
}
}
// Spring automatically catches this and returns HTTP 404
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleNotFound(ResourceNotFoundException e) {
return ResponseEntity.status(404).body(e.getMessage());
}
}Check Your Knowledge
Test your understanding of Custom Exceptions with these quick questions.