Custom Exceptions in Java
A custom exception names a failure in the language of your domain, which makes handling precise and messages meaningful.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Why write your own
- The type itself carries meaning:
InsufficientBalanceExceptionsays more thanIllegalStateException. - Callers can catch exactly the failure they know how to handle.
- Extra data can travel with the exception, such as the account and the shortfall.
- A common base type lets one handler cover a whole subsystem.
An unchecked custom exception
public class InsufficientBalanceException extends RuntimeException {
private final String accountId;
private final long shortfall;
public InsufficientBalanceException(String accountId, long shortfall) {
super("Account " + accountId + " is short by " + shortfall);
this.accountId = accountId;
this.shortfall = shortfall;
}
public String accountId() {
return accountId;
}
public long shortfall() {
return shortfall;
}
}public void withdraw(long amount) {
if (amount > balance) {
throw new InsufficientBalanceException(id, amount - balance);
}
balance -= amount;
}try {
account.withdraw(5000);
} catch (InsufficientBalanceException e) {
System.out.println("Add " + e.shortfall() + " to continue"); // useful detail
}A checked custom exception
public class NoteNotFoundException extends Exception {
private final long noteId;
public NoteNotFoundException(long noteId) {
super("No note with id " + noteId);
this.noteId = noteId;
}
public NoteNotFoundException(long noteId, Throwable cause) {
super("No note with id " + noteId, cause); // always offer this form
this.noteId = noteId;
}
public long noteId() {
return noteId;
}
}Extend Exception for checked, RuntimeException for unchecked. That single choice is the whole difference.
Which one to choose
| Choose checked when | Choose unchecked when |
|---|---|
| The caller can realistically recover | The failure means a bug |
| The failure is an expected external condition | An argument or state was invalid |
| You want the compiler to force a decision | Handling would be pointless boilerplate |
Modern Java leans towards unchecked. Before adding a checked exception, ask what a caller would actually do differently on catching it. If the honest answer is "log it and give up", unchecked is the better choice.
A base type for a subsystem
public class StorageException extends RuntimeException {
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
public class RecordNotFoundException extends StorageException {
public RecordNotFoundException(String message) {
super(message, null);
}
}
public class DuplicateKeyException extends StorageException {
public DuplicateKeyException(String message, Throwable cause) {
super(message, cause);
}
}try {
repository.save(note);
} catch (DuplicateKeyException e) {
return conflict(e.getMessage()); // a specific reaction
} catch (StorageException e) {
return serverError(e); // everything else from storage
}Translating low level exceptions
public Note find(long id) {
try {
return jdbc.queryForNote(id);
} catch (SQLException e) {
throw new StorageException("Lookup failed for note " + id, e);
}
}Callers should depend on your abstraction, not on the fact that storage happens to be SQL today. Wrapping keeps the boundary clean, and the cause keeps the diagnosis.
The four constructors to offer
public class ValidationException extends RuntimeException {
public ValidationException() { }
public ValidationException(String message) { super(message); }
public ValidationException(String message, Throwable cause) { super(message, cause); }
public ValidationException(Throwable cause) { super(cause); }
}Provide at least the message and the message plus cause forms. An exception with no way to attach a cause forces callers to discard information.
Carrying structured data
public class ValidationException extends RuntimeException {
private final List<String> problems;
public ValidationException(List<String> problems) {
super("Validation failed: " + problems.size() + " problem(s)");
this.problems = List.copyOf(problems);
}
public List<String> problems() {
return problems;
}
}This is a real advantage of a custom type. The handler gets the list of problems as data, rather than having to parse a message string.
Common mistakes
- Creating a new exception type for every method, producing dozens that nobody catches individually.
- Extending
Exceptionby reflex, which forcesthrowseverywhere for no benefit. - Omitting the constructor that accepts a cause.
- Putting secrets such as passwords or tokens into the message.
- Extending
ThrowableorErrordirectly. - Naming a class without the
Exceptionsuffix, which breaks a very strong convention.
Best practices
- Add a custom exception only when a caller would treat it differently.
- Name it after the problem and end the name with
Exception. - Prefer unchecked unless recovery is genuinely expected.
- Give a subsystem one base type and a small number of subtypes.
- Include identifying data as fields, not only in the message.
- Make the exception immutable.
Practice
- Write
InvalidCouponExceptioncarrying the coupon code, and catch it to print a specific message. - Decide checked or unchecked for: a malformed configuration file, a negative quantity, a network timeout. Justify each.
- Build a base exception with two subtypes and a handler that treats one specially.
- Why should every custom exception offer a constructor taking a cause?
- Convert a method that returns an error code into one that throws a custom exception, and compare the call sites.
Conclusion
Write a custom exception when the type itself communicates something a caller can act on. Keep the hierarchy small, prefer unchecked, always support a cause, and carry the useful details as fields.