Custom Exceptions in Java
A custom exception is a subclass of Exception or RuntimeException that carries domain-specific information. Create one when a built-in type (IllegalArgumentException, IllegalStateException) doesn't carry enough context, or when callers need to catch a specific type distinctly.
Minimal unchecked exception
public class OrderNotFoundException extends RuntimeException {
private final long orderId;
public OrderNotFoundException(long orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public long orderId() { return orderId; }
}
Standard constructors
Good custom exceptions follow the Throwable four-constructor convention so callers can always chain a cause:
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException() { super(); }
public OrderNotFoundException(String message) { super(message); }
public OrderNotFoundException(String message, Throwable cause){ super(message, cause); }
public OrderNotFoundException(Throwable cause) { super(cause); }
}
Checked or unchecked?
Use checked (extends Exception) when… | Use unchecked (extends RuntimeException) when… |
|---|---|
| The caller has a realistic recovery path. | The error signals a programming bug or unrecoverable state. |
| You're writing a library whose consumers should be forced to decide. | The exception is a "business" error used in large swaths of code (Spring, JPA do this). |
Modern Java trends toward unchecked exceptions — they compose better with lambdas and keep method signatures clean.
Carrying context
public class ValidationException extends RuntimeException {
private final List<String> errors;
public ValidationException(List<String> errors) {
super("Validation failed: " + String.join(", ", errors));
this.errors = List.copyOf(errors);
}
public List<String> errors() { return errors; }
}
Sealed exception hierarchies (Java 17+)
public sealed class ServiceException extends RuntimeException
permits NotFoundException, ConflictException, UnauthorizedException {
protected ServiceException(String m) { super(m); }
}
public final class NotFoundException extends ServiceException { ... }
public final class ConflictException extends ServiceException { ... }
Common mistakes
- Exception per call site — ten near-identical classes instead of one with a field.
- Dropping the cause — always accept a
Throwable causeconstructor. - Extending
Throwabledirectly — useExceptionorRuntimeException. - Putting business logic in the exception — the class is a messenger, not a service.
Related
Pillar: Java exceptions. Siblings: throw, checked vs unchecked.