☕ Java
Custom Exception
A custom exception is a user-defined exception class that extends Exception, RuntimeException, or one of their subclasses. Custom exceptions allow you to represent domain-specific error conditions with meaningful names, carry relevant contextual data about the failure, and enable callers to catch exactly the errors they can handle without catching unrelated exceptions. Well-designed custom exceptions communicate the nature of a failure clearly and give callers the information they need to respond appropriately.
Why Custom Exceptions Exist
Java's standard library provides a comprehensive set of exception classes — IllegalArgumentException, IOException, NullPointerException, and dozens more. These cover general-purpose error conditions that arise in any program. But they cannot cover the domain-specific failure conditions of your application. When an order cannot be fulfilled because the item is out of stock, throwing IllegalStateException tells the caller that something is wrong but gives them no indication of what is wrong or how to respond. An OutOfStockException with the product ID, requested quantity, and available quantity is informative, actionable, and catchable distinctly from other IllegalStateException causes.
Custom exceptions serve three purposes. First, they give names to failure conditions. A name communicates intent — InsufficientFundsException tells the caller exactly what happened without reading the message string. Second, they carry structured data. A custom exception can have fields for the relevant context — account number, requested amount, available balance — that the caller can extract programmatically to display a tailored error message or take a specific action. Third, they enable precise catch clauses. If a method declares throws InsufficientFundsException and throws OrderNotFoundException, callers can handle each case differently with separate catch blocks rather than catching a broad Exception and checking the message string.
Custom exceptions also document the failure contract of a method. A method signature that declares throws CustomerNotFoundException and throws DuplicateEmailException tells the caller exactly what can go wrong and what they should prepare to handle. This is more informative than a generic throws Exception declaration and is enforced by the compiler for checked exceptions.
The decision between making a custom exception checked (extends Exception) or unchecked (extends RuntimeException) follows the same principles as for all exceptions: use checked for conditions the caller is expected to handle, use unchecked for programming errors and conditions where recovery is not generally expected.
Java
// ── Without custom exceptions — poor error communication: ────────────
public Order placeOrder(String productId, int quantity, String customerId) {
if (!inventory.isAvailable(productId, quantity)) {
throw new IllegalStateException("Cannot place order");
// What does this tell the caller? Which product? How many were available?
// How should the caller respond? They have no idea.
}
// ...
}
// Caller cannot distinguish different failure reasons:
try {
order = placeOrder("PROD-001", 5, "CUST-123");
} catch (IllegalStateException e) {
// e.getMessage() = "Cannot place order" — useless for programmatic handling
showGenericError(); // forced to show a generic message
}
// ── With custom exceptions — clear, informative, precise: ─────────────
public Order placeOrder(String productId, int quantity, String customerId)
throws OutOfStockException, CustomerNotFoundException {
// Caller now knows exactly what can go wrong.
if (!inventory.isAvailable(productId, quantity)) {
int available = inventory.getQuantity(productId);
throw new OutOfStockException(productId, quantity, available);
// Carries all relevant context.
}
// ...
}
// Caller handles each case distinctly with full context:
try {
order = placeOrder("PROD-001", 5, "CUST-123");
} catch (OutOfStockException e) {
System.out.printf("Only %d of %s available (you requested %d)%n",
e.getAvailable(), e.getProductId(), e.getRequested());
} catch (CustomerNotFoundException e) {
System.out.println("Customer " + e.getCustomerId() + " not found");
}Creating Custom Checked Exceptions
A custom checked exception extends Exception (or one of its checked subclasses like IOException). Checked exceptions are part of the method's declared contract — the compiler requires every method that throws one to either declare it with throws or catch it. This makes checked exceptions appropriate for anticipated failure conditions where the caller should be required to think about and handle the failure.
Every custom exception should provide at minimum two constructors: one that accepts a message string and one that accepts a message string plus a cause (another Throwable). These mirror the constructors of Exception itself. The cause constructor is critical for wrapping lower-level exceptions — when your code catches a lower-level exception (like SQLException) and wants to throw a higher-level domain exception, the cause preserves the full stack trace of the original failure. Omitting the cause constructor produces exception chaining that loses information, making debugging significantly harder.
A well-designed custom exception class stores relevant contextual fields and provides getter methods for them. The message string produced by getMessage() should be human-readable and include the key facts, but structured data should also be available as typed fields so the caller can extract values programmatically rather than parsing the message string. Parsing exception message strings in production code is a sign that the exception was not designed well.
The class should be final or designed for subclassing intentionally. The constructors that delegate to super() should call the corresponding super constructor — super(message) for message-only, super(message, cause) for chaining, super(cause) for cause-only, and the four-argument form if you need to control suppression and stack trace behaviour.
Java
// ── Custom checked exception with contextual fields: ─────────────────
public class InsufficientFundsException extends Exception {
private final String accountId;
private final double requestedAmount;
private final double availableBalance;
// Constructor 1: message only
public InsufficientFundsException(String accountId,
double requested,
double available) {
super(String.format(
"Insufficient funds in account %s: requested %.2f, available %.2f",
accountId, requested, available));
this.accountId = accountId;
this.requestedAmount = requested;
this.availableBalance = available;
}
// Constructor 2: message + cause (for exception chaining)
public InsufficientFundsException(String accountId,
double requested,
double available,
Throwable cause) {
super(String.format(
"Insufficient funds in account %s: requested %.2f, available %.2f",
accountId, requested, available), cause);
this.accountId = accountId;
this.requestedAmount = requested;
this.availableBalance = available;
}
// Getters for structured access — callers need not parse the message:
public String getAccountId() { return accountId; }
public double getRequestedAmount() { return requestedAmount; }
public double getAvailableBalance(){ return availableBalance; }
}
// ── Using the custom exception: ───────────────────────────────────────
public class BankingService {
public void transfer(String fromAccountId, String toAccountId,
double amount)
throws InsufficientFundsException {
Account from = accountRepository.findById(fromAccountId);
if (from.getBalance() < amount) {
throw new InsufficientFundsException(
fromAccountId, amount, from.getBalance());
}
// perform transfer...
}
}
// ── Handling with structured data: ───────────────────────────────────
try {
bankingService.transfer("ACC-001", "ACC-002", 5000.00);
} catch (InsufficientFundsException e) {
System.out.printf(
"Transfer failed: account %s has %.2f but %.2f was requested%n",
e.getAccountId(), e.getAvailableBalance(), e.getRequestedAmount());
// Programmatic access — no message parsing needed.
}Creating Custom Unchecked Exceptions
A custom unchecked exception extends RuntimeException. Unlike checked exceptions, the compiler does not require callers to declare or catch them. Unchecked exceptions are appropriate for programming errors, violated preconditions, and exceptional conditions that most callers cannot reasonably recover from.
The design principles are the same as for checked exceptions — provide meaningful names, carry contextual fields, and include constructors for message, message-plus-cause, and cause-only. The primary difference is that the calling code can choose to catch them or let them propagate up to a top-level error handler, and the compiler does not enforce this choice.
Extending RuntimeException rather than Exception does not reduce the quality of the exception — the exception should still be named clearly, carry relevant data, and have a useful message. The choice of base class is about whether callers are required to handle the exception (checked) or have the option to (unchecked).
Many frameworks and APIs use unchecked exceptions exclusively because checked exceptions add significant verbosity to client code — every method that could throw must declare it, and every caller must handle it or propagate it. Spring, Hibernate, and most modern Java frameworks use unchecked exceptions for this reason, relying on global error handlers to deal with failures rather than requiring every layer of code to handle every possible exception.
Java
// ── Custom unchecked exception: ──────────────────────────────────────
public class OrderNotFoundException extends RuntimeException {
private final String orderId;
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public OrderNotFoundException(String orderId, Throwable cause) {
super("Order not found: " + orderId, cause);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
// ── Custom unchecked for validation failures: ─────────────────────────
public class ValidationException extends RuntimeException {
private final String field;
private final Object rejectedValue;
private final String constraint;
public ValidationException(String field, Object value, String constraint) {
super(String.format(
"Validation failed for field '%s': value '%s' violates constraint '%s'",
field, value, constraint));
this.field = field;
this.rejectedValue = value;
this.constraint = constraint;
}
public ValidationException(String field, Object value,
String constraint, Throwable cause) {
super(String.format(
"Validation failed for field '%s': value '%s' violates constraint '%s'",
field, value, constraint), cause);
this.field = field;
this.rejectedValue = value;
this.constraint = constraint;
}
public String getField() { return field; }
public Object getRejectedValue() { return rejectedValue; }
public String getConstraint() { return constraint; }
}
// ── Using unchecked exceptions: ───────────────────────────────────────
public class OrderService {
// No throws declaration needed — unchecked propagates automatically:
public Order findById(String orderId) {
Order order = orderRepository.findById(orderId);
if (order == null) {
throw new OrderNotFoundException(orderId);
}
return order;
}
public void createOrder(CreateOrderRequest request) {
if (request.getQuantity() <= 0) {
throw new ValidationException("quantity",
request.getQuantity(), "must be positive");
}
// ...
}
}
// ── Callers may handle or let propagate: ─────────────────────────────
// Handling at the controller level:
try {
Order order = orderService.findById(orderId);
return ResponseEntity.ok(order);
} catch (OrderNotFoundException e) {
return ResponseEntity.notFound().build();
}
// Or let a global exception handler deal with it — no try/catch needed.Exception Hierarchy Design and Best Practices
For systems with multiple related error conditions, designing an exception hierarchy produces more maintainable code than many unrelated exception classes. A hierarchy allows callers to catch at the appropriate level of specificity — catching the base exception class handles all subclass exceptions, while catching a specific subclass handles just that one. This gives callers flexibility in how precisely they want to handle failures.
A base application exception — often called AppException or DomainException — sits at the root of the hierarchy and extends RuntimeException (or Exception for checked hierarchies). All application-specific exceptions extend from this base. This means a caller who wants to handle "any application error" can catch AppException, while a caller who wants to handle "specifically a not-found error" can catch NotFoundException.
The hierarchy should be deep enough to be useful but not so deep that it becomes difficult to navigate. Two to three levels is usually sufficient: base exception → category exception (NotFoundException, ValidationException, AuthorizationException) → specific exception (OrderNotFoundException, CustomerNotFoundException). Going deeper typically indicates that a different mechanism — error codes, result types — might serve better.
Exception wrapping (catching one exception and throwing another) should always preserve the cause. When your code catches a low-level technical exception (like a database exception) and wraps it in a domain exception, passing the original as the cause preserves the full diagnostic information. Losing the cause — the stack trace of the original failure — makes debugging significantly harder and should be treated as a serious code quality issue.
Java
// ── Exception hierarchy for an e-commerce domain: ────────────────────
// Base exception — all domain exceptions extend this:
public class ECommerceException extends RuntimeException {
public ECommerceException(String message) {
super(message);
}
public ECommerceException(String message, Throwable cause) {
super(message, cause);
}
}
// Category: resource not found:
public class ResourceNotFoundException extends ECommerceException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// Category: business rule violation:
public class BusinessRuleException extends ECommerceException {
public BusinessRuleException(String message) {
super(message);
}
public BusinessRuleException(String message, Throwable cause) {
super(message, cause);
}
}
// Specific: order not found:
public class OrderNotFoundException extends ResourceNotFoundException {
private final String orderId;
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
// Specific: product out of stock:
public class OutOfStockException extends BusinessRuleException {
private final String productId;
private final int requested;
private final int available;
public OutOfStockException(String productId, int requested, int available) {
super(String.format("Product %s: requested %d, available %d",
productId, requested, available));
this.productId = productId;
this.requested = requested;
this.available = available;
}
public String getProductId() { return productId; }
public int getRequested() { return requested; }
public int getAvailable() { return available; }
}
// ── Callers catch at the appropriate level: ───────────────────────────
try {
orderService.placeOrder(request);
} catch (OutOfStockException e) {
// Specific handling — show stock information:
showOutOfStockMessage(e.getProductId(), e.getAvailable());
} catch (BusinessRuleException e) {
// Catch all other business rule violations:
showBusinessError(e.getMessage());
} catch (ECommerceException e) {
// Catch anything domain-related:
showGeneralError(e.getMessage());
}
// ── Exception wrapping — always preserve the cause: ───────────────────
public Order findOrder(String orderId) {
try {
return database.query("SELECT * FROM orders WHERE id = ?", orderId);
} catch (SQLException e) {
// Wrap in domain exception, preserve cause for diagnosis:
throw new OrderNotFoundException(orderId, e); // cause preserved!
}
}Related Topics in Exception Handling
Exception Basics
An exception is an event that disrupts the normal flow of a program during execution. Java's exception system is a structured mechanism for signalling, propagating, and handling error conditions — a significant improvement over the older approach of returning special error codes that callers could silently ignore. Understanding exceptions means understanding the class hierarchy that categorises them, the distinction between checked and unchecked exceptions, what the JVM does when an exception is thrown, how the call stack is unwound, and what information an exception object carries. This entry covers the full exception hierarchy, the checked vs unchecked distinction and the reasoning behind it, exception propagation through the call stack, and the information model of exception objects.
try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
Multiple catch
A single try block can be followed by multiple catch blocks, each handling a different exception type. Multiple catch blocks allow code to respond differently to different kinds of failures — recovering from an expected missing file, but reporting and rethrowing an unexpected database error. Java 7 introduced multi-catch syntax, allowing a single catch block to handle multiple unrelated exception types, eliminating the duplicate code that arises when different exceptions require the same handling. This entry covers the ordering rules, multi-catch syntax, the type of the caught variable in multi-catch, and the design patterns for structuring multiple exception handlers.
finally
The finally block contains code that must execute regardless of whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught. It is the mechanism for guaranteed cleanup — closing files, releasing locks, returning connections to a pool, rolling back transactions. The finally block runs in all scenarios except two: if the JVM exits (System.exit()) or if the thread is killed by an Error like StackOverflowError. Understanding when exactly finally runs, the interaction between finally and return statements, exception suppression when finally itself throws, and the modern alternative of try-with-resources is essential for writing correct resource management code in Java.