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
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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:
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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:
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
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()); }.
Continue the lesson path