Custom Exceptions — Creating Exception Types, Chained Exceptions, and Best Practices
In this tutorial, you will learn about Custom Exceptions. We cover key concepts, practical examples, and best practices to help you master this topic.
Custom exceptions in Java extend Exception or RuntimeException to represent domain-specific error conditions. While Java provides many built-in exception types, domain-specific exceptions like InsufficientFundsException, BookNotFoundException, or PaymentDeclinedException make error handling more meaningful and allow callers to react appropriately.
What You'll Learn
- Creating custom checked and unchecked exceptions
- Exception chaining to preserve the root cause
- Best practices for exception design
Why It Matters
Custom exceptions improve code readability and maintainability. A method that throws BookNotFoundException communicates intent far better than one that throws a generic Exception. Catch blocks become more precise — you handle each failure case according to its domain semantics.
Real-World Use
Spring throws DataAccessException (hierarchical), JPA throws EntityNotFoundException, and every REST API defines custom exceptions mapped to specific HTTP status codes.
Creating a Custom Exception
Checked Exception
public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
public InsufficientFundsException(String message, Throwable cause) {
super(message, cause);
}
}
Usage:
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(
"Insufficient funds: balance=" + balance + ", requested=" + amount
);
}
balance -= amount;
}
Unchecked Exception
public class InvalidOrderStateException extends RuntimeException {
public InvalidOrderStateException(String message) {
super(message);
}
public InvalidOrderStateException(String message, Throwable cause) {
super(message, cause);
}
}
Exception Chaining
When an exception occurs at a low level, you often want to rethrow it as a higher-level exception while preserving the original cause:
public void processOrder(Order order) {
try {
saveToDatabase(order);
sendConfirmationEmail(order);
} catch (DatabaseException e) {
throw new OrderProcessingException("Failed to process order: " + order.id(), e);
}
}
The original exception is accessible via getCause():
try {
service.processOrder(order);
} catch (OrderProcessingException e) {
Throwable cause = e.getCause();
if (cause instanceof DatabaseException) {
// handle database-specific recovery
}
}
Suppressed Exceptions
When using try-with-resources, if both the try block and the close method throw exceptions, the close exception is suppressed. Suppressed exceptions are stored in the thrown exception:
try (Resource r = new Resource()) {
throw new IOException("Try failed");
} catch (IOException e) {
Throwable[] suppressed = e.getSuppressed();
// suppressed[0] is the exception from close()
}
Pattern: Exception Hierarchy
Organize custom exceptions in a hierarchy for consistent handling:
// Base exception for the entire application
public class ApplicationException extends RuntimeException {
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
}
// Domain-specific exceptions
public class PaymentException extends ApplicationException {
public PaymentException(String message, Throwable cause) {
super(message, cause);
}
}
public class InsufficientFundsException extends PaymentException {
private final double balance;
private final double requested;
public InsufficientFundsException(double balance, double requested) {
super("Insufficient funds: " + balance + " < " + requested, null);
this.balance = balance;
this.requested = requested;
}
public double getBalance() { return balance; }
public double getRequested() { return requested; }
}
public class PaymentDeclinedException extends PaymentException {
public PaymentDeclinedException(String reason, Throwable cause) {
super("Payment declined: " + reason, cause);
}
}
This hierarchy lets callers catch at the appropriate level:
try {
paymentService.charge(amount);
} catch (InsufficientFundsException e) {
// suggest deposit
} catch (PaymentException e) {
// generic payment failure
}
Best Practices
- Provide meaningful messages. Include relevant state:
"Book not found: id=42"not"Not found". - Include the cause chain. Always pass the original exception when wrapping.
- Make exceptions actionable. The exception message should help the caller decide what to do.
- Use checked exceptions for recoverable errors. If the caller can reasonably recover (file not found, network timeout), make it checked.
- Use unchecked exceptions for programming errors. IllegalArgumentException, IllegalStateException — these indicate bugs.
- Consider adding fields for context.
InsufficientFundsExceptionwithgetBalance()andgetRequested()helps the caller.
Common Mistakes
- Creating a flat exception hierarchy. A single
CustomExceptionclass with different messages forces callers to parse strings. Use subclasses instead. - Swallowing the cause in exception chaining.
throw new MyException("failed")without passing the original loses the root cause. Always passeto the constructor. - Overusing checked exceptions. Every method in a deep call chain must declare the checked exception, making changes painful. Prefer unchecked for most application exceptions.
- Including sensitive data in messages. Never include passwords, credit card numbers, or personal data in exception messages (these may be logged).
- Throwing generic exceptions.
throw new Exception("something broke")forces callers to catch or declareException, defeating the purpose.
Practice Questions
1. When should you create a checked custom exception vs an unchecked one?
Checked: for recoverable conditions where the caller should decide the response. Unchecked: for programming errors or conditions the caller cannot reasonably handle.
2. What is exception chaining?
Wrapping a lower-level exception in a higher-level one while preserving the original via the cause parameter.
3. What information should a custom exception include?
A meaningful message, the original cause (if wrapping), and relevant fields (like account balance, order ID) that help the caller recover.
4. How do you access suppressed exceptions?
Via Throwable.getSuppressed(). Suppressed exceptions occur when multiple exceptions are thrown (e.g., try-with-resources).
5. Why should you pass the cause when wrapping an exception?
To preserve the original stack trace and error information. Without the cause, the root cause is lost.
Challenge Question:
Design an exception hierarchy for a library management system. Include LibraryException (base), BookNotFoundException, MemberNotFoundException, BookNotAvailableException, and LateReturnFeeException. Each should carry relevant fields (book ISBN, member ID, fee amount). Write a method that throws different exceptions depending on the failure and a catch block that handles each specifically.
FAQ
Mini Project
Write a program CustomExceptionsDemo.java that:
- Defines
AccountNotFoundException,InsufficientFundsException, andAccountFrozenException— all checked exceptions - Each exception carries relevant state fields (account ID, balance, requested amount)
- Creates a
BankServiceclass that reads accounts from a CSV file and providestransfer(fromId, toId, amount) - The
transfermethod throws specific exceptions for each failure mode - A
Mainclass that invokestransferand catches each exception type separately, printing recovery suggestions - Demonstrates exception chaining when a file I/O error occurs while loading accounts
What's Next
Custom exceptions help you handle errors gracefully. Now it is time to work with collections — Java's framework for storing and manipulating groups of objects. Lesson 23 covers the List interface and its implementations: ArrayList, LinkedList, Vector, and CopyOnWriteArrayList.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro