Java Notes
Complete guide to creating custom exceptions in Java — when to create them, checked vs unchecked custom exceptions, best practices, exception chaining, and real-world patterns for enterprise applications.
Why Create Custom Exceptions?
Java provides many built-in exceptions, but they're generic. In real applications, you need domain-specific exceptions that:
- Communicate specific business errors —
InsufficientFundsExceptionis clearer thanRuntimeException("not enough money") - Carry additional context — error codes, affected entities, recovery suggestions
- Enable fine-grained handling — callers can catch specific business errors separately
- Improve API design — method signatures clearly document what can go wrong
Creating a Basic Custom Exception
Custom Checked Exception (extends Exception)
// Custom checked exception — caller MUST handle or declare
public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}Custom Unchecked Exception (extends RuntimeException)
// Custom unchecked exception — caller doesn't need to handle
public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}When to Choose Checked vs Unchecked
Use Checked (extends Exception) | Use Unchecked (extends RuntimeException) |
|---|---|
| Caller can meaningfully recover | Programming error / bug |
| External resource failures | Validation failures |
| Business rule violations with alternatives | Illegal state that shouldn't happen |
| API contracts the caller must respect | Framework/internal errors |
Complete Custom Exception with All Constructors
A well-designed custom exception should have four constructors matching Exception:
public class ApplicationException extends Exception {
// 1. No-arg constructor
public ApplicationException() {
super();
}
// 2. Message constructor
public ApplicationException(String message) {
super(message);
}
// 3. Message + Cause constructor (for exception chaining)
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
// 4. Cause-only constructor
public ApplicationException(Throwable cause) {
super(cause);
}
}Real-World Example: Banking System
// === Custom Exceptions ===
class InsufficientFundsException extends Exception {
private double balance;
private double withdrawAmount;
public InsufficientFundsException(double balance, double withdrawAmount) {
super(String.format(
"Cannot withdraw $%.2f. Available balance: $%.2f. Short by: $%.2f",
withdrawAmount, balance, withdrawAmount - balance));
this.balance = balance;
this.withdrawAmount = withdrawAmount;
}
public double getBalance() { return balance; }
public double getWithdrawAmount() { return withdrawAmount; }
public double getDeficit() { return withdrawAmount - balance; }
}
class AccountNotFoundException extends Exception {
private String accountId;
public AccountNotFoundException(String accountId) {
super("Account not found: " + accountId);
this.accountId = accountId;
}
public String getAccountId() { return accountId; }
}
class AccountLockedException extends Exception {
private String accountId;
private String reason;
public AccountLockedException(String accountId, String reason) {
super("Account " + accountId + " is locked: " + reason);
this.accountId = accountId;
this.reason = reason;
}
public String getAccountId() { return accountId; }
public String getReason() { return reason; }
}
// === Bank Account Class ===
class BankAccount {
private String accountId;
private String ownerName;
private double balance;
private boolean locked;
private String lockReason;
public BankAccount(String accountId, String ownerName, double balance) {
this.accountId = accountId;
this.ownerName = ownerName;
this.balance = balance;
this.locked = false;
}
public void withdraw(double amount)
throws InsufficientFundsException, AccountLockedException {
if (locked) {
throw new AccountLockedException(accountId, lockReason);
}
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive: " + amount);
}
if (amount > balance) {
throw new InsufficientFundsException(balance, amount);
}
balance -= amount;
System.out.printf(" ✓ Withdrew $%.2f from %s. New balance: $%.2f%n",
amount, accountId, balance);
}
public void lock(String reason) {
this.locked = true;
this.lockReason = reason;
}
public double getBalance() { return balance; }
public String getAccountId() { return accountId; }
}
// === Banking Service ===
public class BankingSystem {
private static java.util.Map<String, BankAccount> accounts = new java.util.HashMap<>();
static {
accounts.put("ACC001", new BankAccount("ACC001", "Alice", 5000));
accounts.put("ACC002", new BankAccount("ACC002", "Bob", 1200));
BankAccount locked = new BankAccount("ACC003", "Charlie", 8000);
locked.lock("Suspicious activity detected");
accounts.put("ACC003", locked);
}
static BankAccount findAccount(String accountId) throws AccountNotFoundException {
BankAccount account = accounts.get(accountId);
if (account == null) {
throw new AccountNotFoundException(accountId);
}
return account;
}
static void processWithdrawal(String accountId, double amount) {
System.out.printf("Processing withdrawal: $%.2f from %s%n", amount, accountId);
try {
BankAccount account = findAccount(accountId);
account.withdraw(amount);
} catch (AccountNotFoundException e) {
System.out.println(" ✗ " + e.getMessage());
System.out.println(" → Please verify account number");
} catch (InsufficientFundsException e) {
System.out.println(" ✗ " + e.getMessage());
System.out.printf(" → Consider withdrawing up to $%.2f%n",
e.getBalance());
} catch (AccountLockedException e) {
System.out.println(" ✗ " + e.getMessage());
System.out.println(" → Contact support at 1-800-BANK");
} catch (IllegalArgumentException e) {
System.out.println(" ✗ Invalid input: " + e.getMessage());
}
}
public static void main(String[] args) {
System.out.println("=== Banking System Demo ===\n");
processWithdrawal("ACC001", 3000); // Success
System.out.println();
processWithdrawal("ACC001", 5000); // Insufficient funds
System.out.println();
processWithdrawal("ACC999", 100); // Account not found
System.out.println();
processWithdrawal("ACC003", 500); // Account locked
System.out.println();
processWithdrawal("ACC002", -50); // Invalid amount
}
}Output:
| Processing withdrawal | $3000.00 from ACC001 |
| ✓ Withdrew $3000.00 from ACC001. New balance | $2000.00 |
| Processing withdrawal | $5000.00 from ACC001 |
| ✗ Cannot withdraw $5000.00. Available balance | $2000.00. Short by: $3000.00 |
| Processing withdrawal | $100.00 from ACC999 |
| ✗ Account not found | ACC999 |
| Processing withdrawal | $500.00 from ACC003 |
| ✗ Account ACC003 is locked | Suspicious activity detected |
| Processing withdrawal | $-50.00 from ACC002 |
| ✗ Invalid input | Amount must be positive: -50.0 |
Custom Exception with Error Codes
Enterprise applications often use error codes for API responses:
Output:
| Found user | USR001 |
| API Error | [1001] Validation Failed: User ID is required | Details: {field=userId, constraint=not_empty} |
| Code | 1001 |
| Details | {field=userId, constraint=not_empty} |
| API Error | [1002] Resource Not Found: User not found | Details: {userId=INVALID_123, searchedAt=2024-01-15T10:30:00Z} |
| Code | 1002 |
| Details | {userId=INVALID_123, searchedAt=2024-01-15T10:30:00Z} |
Exception Chaining (Wrapping)
When a low-level exception occurs, you should wrap it in a higher-level exception:
import java.io.*;
class DataLoadException extends Exception {
private String filename;
public DataLoadException(String message, String filename, Throwable cause) {
super(message, cause);
this.filename = filename;
}
public String getFilename() { return filename; }
}
class ConfigurationException extends Exception {
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
public class ExceptionChainingDemo {
static java.util.Properties loadConfig(String filename)
throws ConfigurationException {
try {
FileInputStream fis = new FileInputStream(filename);
java.util.Properties props = new java.util.Properties();
props.load(fis);
fis.close();
return props;
} catch (FileNotFoundException e) {
throw new ConfigurationException(
"Configuration file not found: " + filename, e);
} catch (IOException e) {
throw new ConfigurationException(
"Error reading configuration: " + filename, e);
}
}
public static void main(String[] args) {
try {
loadConfig("app.properties");
} catch (ConfigurationException e) {
System.out.println("=== Exception Chain ===");
System.out.println("Top-level: " + e.getMessage());
System.out.println("Cause: " + e.getCause().getClass().getSimpleName()
+ " - " + e.getCause().getMessage());
System.out.println("\n=== Full Stack Trace ===");
e.printStackTrace(System.out);
}
}
}Output:
| Top-level | Configuration file not found: app.properties |
| Cause | FileNotFoundException - app.properties (No such file or directory) |
| ConfigurationException | Configuration file not found: app.properties |
| at ExceptionChainingDemo.loadConfig(ExceptionChainingDemo.java | 27) |
| at ExceptionChainingDemo.main(ExceptionChainingDemo.java | 36) |
| Caused by | java.io.FileNotFoundException: app.properties (No such file or directory) |
| at java.io.FileInputStream.<init>(FileInputStream.java | 157) |
| at ExceptionChainingDemo.loadConfig(ExceptionChainingDemo.java | 22) |
Exception Hierarchy for a Real Application
Output:
Common Mistakes
1. Not extending the right parent class
// ❌ Extending Throwable directly (too broad)
class MyException extends Throwable { }
// ❌ Extending Error (reserved for JVM issues)
class MyException extends Error { }
// ✅ Extend Exception (checked) or RuntimeException (unchecked)
class MyCheckedException extends Exception { }
class MyUncheckedException extends RuntimeException { }2. Losing the original cause
// ❌ BAD: Original exception cause is LOST
try {
// code
} catch (SQLException e) {
throw new AppException("DB Error"); // Where's the cause?
}
// ✅ GOOD: Preserve the cause
try {
// code
} catch (SQLException e) {
throw new AppException("DB Error", e); // Cause preserved!
}3. Making exceptions mutable
// ❌ BAD: Mutable exception
class BadException extends Exception {
private String detail;
public void setDetail(String d) { this.detail = d; } // Mutable!
}
// ✅ GOOD: Immutable exception
class GoodException extends Exception {
private final String detail; // final!
public GoodException(String message, String detail) {
super(message);
this.detail = detail;
}
public String getDetail() { return detail; }
}Best Practices
- Name ends with "Exception" —
InvalidOrderException, notInvalidOrder - Provide all four standard constructors when possible
- Include the original cause in chained exceptions
- Make exception fields final (immutable)
- Add meaningful context — error codes, affected IDs, suggestions
- Create an exception hierarchy for complex domains
- Use checked for recoverable, unchecked for bugs
- Document with @throws in Javadoc
- Make exceptions Serializable (they already are via Throwable)
Interview Questions
Q1: How do you create a custom exception in Java?
Answer: Extend Exception for checked or RuntimeException for unchecked. At minimum, provide a constructor that takes a String message and calls super(message). Best practice: provide all four constructors (no-arg, message, message+cause, cause-only).
Q2: When should you create a custom exception vs using built-in ones?
Answer: Create custom exceptions when: (1) you need to carry domain-specific data (account ID, error codes), (2) callers need to distinguish your errors from other exceptions, (3) you want a hierarchy of related exceptions. Use built-in ones for generic cases (IllegalArgumentException for bad inputs, IllegalStateException for bad state).
Q3: Should custom exceptions be checked or unchecked?
Answer: Use checked when the caller can take meaningful recovery action (retry, use default, prompt user). Use unchecked when it's a programming error that should be fixed in code. In modern practice, unchecked exceptions are generally preferred because they don't clutter method signatures.
Q4: What is exception chaining and why is it important?
Answer: Exception chaining wraps a low-level exception in a higher-level one using new MyException("message", cause). It's important because: (1) it preserves the full error history for debugging, (2) it hides implementation details from callers, (3) the original cause is accessible via getCause().
Q5: What methods should a well-designed custom exception have?
Answer: At minimum: four constructors (matching Exception), getMessage() (inherited), and any domain-specific getters for additional fields. For enterprise apps: error code, timestamp, and a toMap() or toJson() method for API responses.
Q8: Should custom exceptions be checked or unchecked?
A: It depends on whether the caller can meaningfully recover. Checked (extend Exception): for recoverable conditions like InsufficientFundsException, FileFormatException. Unchecked (extend RuntimeException): for programming errors or conditions callers can't handle like InvalidConfigException, DataCorruptionException.
Q9: What constructors should a custom exception have?
A: At minimum: (1) no-arg constructor, (2) constructor with message (String), (3) constructor with message and cause (String, Throwable), (4) constructor with cause only (Throwable). These mirror the constructors in Exception/RuntimeException and enable proper exception chaining.
Q10: Should custom exceptions be serializable?
A: Yes. All exceptions extend Throwable which implements Serializable. If your custom exception has non-serializable fields (like a database Connection), make them transient. If your exception carries custom data (error codes, context objects), ensure those are serializable or transient too.
Q11: What is the best practice for custom exception class naming?
A: Always end with "Exception": InsufficientBalanceException, InvalidOrderException. For errors (unrecoverable), end with "Error": ConfigurationError. Use business-domain names, not technical names. Group related exceptions with a common parent: PaymentException → InsufficientFundsException, CardDeclinedException.
Q12: How do you add custom data to exceptions?
A: Add fields with getters: private final String errorCode; private final Map<String, Object> context;. Initialize in constructors. This lets catch blocks access structured error data beyond just the message: catch (PaymentException e) { log(e.getErrorCode(), e.getTransactionId()); }.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Custom Exceptions in Java.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, exception, handling, custom
Related Java Master Course Topics