Creating Custom Exceptions and Debugging Exception-Related Problems in Java
Course: Core Java Training
Topic: Creating Custom Exceptions and Debugging Exception-Related Problems
Course Link: Core Java Training Course
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction
Exception handling is an essential part of Java programming. It helps developers manage unexpected conditions, prevent uncontrolled program termination, and provide meaningful feedback when an operation fails. Java includes built-in exception classes such as IllegalArgumentException, IOException, and NullPointerException. However, real-world applications often require exception types that describe specific business rules or application failures.
Custom exceptions are user-defined classes that represent application-specific problems. For example, a banking application might define InsufficientBalanceException, while a student management system might define InvalidMarksException.
Debugging exception-related problems means identifying where an exception occurs, understanding its cause, examining stack traces, inspecting variable values, and correcting the underlying issue rather than merely hiding the error.
2. Learning Objectives
- Understand why custom exceptions are useful.
- Differentiate between checked and unchecked custom exceptions.
- Create custom exceptions by extending
Exception and RuntimeException.
- Use constructors to provide meaningful exception messages.
- Throw and handle custom exceptions using
throw, throws, try, and catch.
- Understand exception chaining and preserve the original cause.
- Read and interpret Java stack traces.
- Debug exceptions using breakpoints, logging, and IDE debugging tools.
- Recognize common exception-handling mistakes and apply best practices.
3. What Is a Custom Exception?
A custom exception is an exception class created by a developer to represent a particular error condition in an application. It extends an existing exception class and can include constructors, messages, and other application-specific information.
Custom exceptions make code easier to understand because their names communicate the meaning of a failure more clearly than a generic exception.
Example of a Business Rule
Imagine a banking application that allows a customer to withdraw money. If the withdrawal amount is greater than the available balance, the operation should be rejected. A custom exception named InsufficientBalanceException clearly describes this condition.
class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
In this example:
InsufficientBalanceException is the custom exception class.
extends Exception makes it a checked exception.
- The constructor accepts a descriptive message.
super(message) passes the message to the parent exception class.
4. Types of Custom Exceptions
Custom exceptions are commonly divided into checked and unchecked exceptions, depending on the superclass they extend.
4.1 Custom Checked Exception
A custom checked exception extends Exception directly or another checked exception class. The compiler requires callers to catch it or declare it with throws.
class InvalidMarksException extends Exception {
public InvalidMarksException(String message) {
super(message);
}
}
This exception is suitable when the application wants callers to explicitly handle a condition, such as invalid student marks or an application-specific validation failure.
4.2 Custom Unchecked Exception
A custom unchecked exception extends RuntimeException. The compiler does not require methods to catch or declare it.
class InvalidProductPriceException extends RuntimeException {
public InvalidProductPriceException(String message) {
super(message);
}
}
This type is often suitable for invalid arguments, illegal object states, or programming-contract violations. The appropriate choice depends on the application's design and how callers are expected to respond.
Comparison Table
| Feature |
Checked Custom Exception |
Unchecked Custom Exception |
| Typical superclass |
Exception |
RuntimeException |
| Compiler requirement |
Must be caught or declared. |
No catch-or-declare requirement. |
| Typical purpose |
Conditions callers are expected to handle explicitly. |
Invalid arguments, illegal states, or programming-contract violations. |
| Declaration example |
class MyException extends Exception |
class MyException extends RuntimeException |
5. Steps to Create a Custom Exception
- Identify the application-specific problem: Determine which condition needs its own exception type.
- Choose the exception category: Decide whether a checked or unchecked exception best represents the situation.
- Create the exception class: Extend
Exception or RuntimeException, depending on the chosen design.
- Add constructors: Provide constructors for a message and, when useful, an underlying cause.
- Throw the exception: Use the
throw keyword when the relevant condition occurs.
- Handle or propagate it: Catch the exception where recovery is possible or declare it when it should be handled by a caller.
- Test the behavior: Verify both valid operations and failure conditions.
6. Creating and Using a Checked Custom Exception
The following example creates a custom exception for a bank account. The withdrawal operation rejects an amount that exceeds the available balance.
class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
class BankAccount {
private double balance;
public BankAccount(double balance) {
if (balance < 0) {
throw new IllegalArgumentException(
"Initial balance cannot be negative."
);
}
this.balance = balance;
}
public void withdraw(double amount)
throws InsufficientBalanceException {
if (amount <= 0) {
throw new IllegalArgumentException(
"Withdrawal amount must be positive."
);
}
if (amount > balance) {
throw new InsufficientBalanceException(
"Insufficient balance. Available balance: " + balance
);
}
balance -= amount;
System.out.println("Withdrawal successful.");
System.out.println("Remaining balance: " + balance);
}
}
public class BankAccountExample {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000.0);
try {
account.withdraw(1500.0);
} catch (InsufficientBalanceException e) {
System.out.println("Transaction failed: " + e.getMessage());
}
}
}
Output
Transaction failed: Insufficient balance. Available balance: 1000.0
Explanation
InsufficientBalanceException represents a specific banking rule violation.
- The
withdraw() method declares the checked exception with throws.
- The
throw statement creates and throws the exception when the amount exceeds the balance.
- The calling code handles the exception in a matching
catch block.
- The message explains why the transaction could not be completed.
The example uses IllegalArgumentException for invalid withdrawal amounts and a custom checked exception for insufficient funds. This distinguishes invalid input from a validly formatted request that cannot be completed under the business rules.
7. Creating a Custom Unchecked Exception
A custom unchecked exception can be used when a method receives an invalid argument or encounters an illegal state that callers are not required to declare or catch.
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
public class AgeValidator {
static void validateAge(int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException(
"Age must be between 0 and 150."
);
}
System.out.println("Age is valid.");
}
public static void main(String[] args) {
try {
validateAge(200);
} catch (InvalidAgeException e) {
System.out.println(e.getMessage());
}
}
}
Output
Age must be between 0 and 150.
Explanation: The custom exception extends RuntimeException, so the compiler does not require validateAge() to declare it. The example catches it to display a clear message, but catching it is not a compile-time requirement.
8. Adding Constructors to Custom Exceptions
Custom exception classes commonly provide constructors that accept a message and an underlying cause. These constructors make it easier to describe a failure and preserve diagnostic information from a lower-level operation.
class ApplicationException extends Exception {
public ApplicationException() {
super();
}
public ApplicationException(String message) {
super(message);
}
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
public ApplicationException(Throwable cause) {
super(cause);
}
}
| Constructor |
Purpose |
ApplicationException() |
Creates an exception without a custom detail message. |
ApplicationException(String message) |
Provides a descriptive message. |
ApplicationException(String message, Throwable cause) |
Provides a message and preserves the underlying cause. |
ApplicationException(Throwable cause) |
Wraps an existing cause. |
Constructors should be included when they serve the needs of the application. A small custom exception does not always need every possible constructor.
9. Throwing and Declaring Custom Exceptions
Java provides two related keywords for exception handling: throw and throws.
The throw Keyword
The throw keyword throws a specific throwable object when a condition is detected.
throw new InvalidAgeException("Age is outside the permitted range.");
The throws Keyword
The throws keyword declares throwable types that a method may propagate to its caller. It is required for checked exceptions that are not handled inside the method.
static void processPayment()
throws ApplicationException {
// Payment processing logic
}
| Keyword |
Purpose |
Location |
throw |
Throws an exception object. |
Inside a method or block. |
throws |
Declares throwable types that may be propagated. |
In a method or constructor declaration. |
10. What Is Exception Chaining?
Exception chaining is the process of preserving an underlying exception as the cause of a higher-level exception. It is useful when a lower-level technical failure needs to be translated into a meaningful application-level failure.
For example, a service may convert a database-related exception into a custom CustomerServiceException while preserving the original database exception for debugging.
class CustomerServiceException extends Exception {
public CustomerServiceException(String message, Throwable cause) {
super(message, cause);
}
}
public class ExceptionChainingExample {
static void loadCustomer() throws CustomerServiceException {
try {
Integer.parseInt("customer");
} catch (NumberFormatException e) {
throw new CustomerServiceException(
"Unable to process customer data.", e
);
}
}
public static void main(String[] args) {
try {
loadCustomer();
} catch (CustomerServiceException e) {
System.out.println(e.getMessage());
System.out.println("Original cause: " + e.getCause());
}
}
}
Explanation
NumberFormatException is the original exception.
CustomerServiceException provides a higher-level description.
- The constructor receives the original exception as the cause.
getCause() retrieves the underlying exception.
Preserving the cause makes debugging easier because the original failure details remain available even when the exception is translated.
11. Introduction to Debugging Exception-Related Problems
Debugging is the process of locating, understanding, and correcting problems in a program. When an exception occurs, the goal is not simply to catch it. Developers should identify the underlying cause and determine whether the program's logic, input, configuration, or environment needs to be corrected.
Exception-related debugging commonly involves:
- Reading the exception class and message.
- Examining the stack trace.
- Finding the line where the exception originated.
- Inspecting variable values and program state.
- Reproducing the problem consistently.
- Correcting the root cause and testing the fix.
12. Understanding a Java Stack Trace
A stack trace describes the sequence of method calls active when an exception was thrown. It helps developers identify the location of the failure and how execution reached that point.
Example Program
public class StackTraceExample {
static void calculate() {
int result = 10 / 0;
System.out.println(result);
}
static void process() {
calculate();
}
public static void main(String[] args) {
process();
}
}
When this program runs, Java throws an ArithmeticException because integer division by zero is invalid.
Illustrative Stack Trace
Exception in thread "main" java.lang.ArithmeticException: / by zero
at StackTraceExample.calculate(StackTraceExample.java:3)
at StackTraceExample.process(StackTraceExample.java:7)
at StackTraceExample.main(StackTraceExample.java:11)
Note: The line numbers above are illustrative and depend on the exact source file.
How to Read a Stack Trace
- Read the exception type:
ArithmeticException identifies the category of failure.
- Read the message:
/ by zero describes the immediate problem.
- Locate the first relevant application frame: The top application frame usually points to where the exception was thrown.
- Follow the remaining frames: These show the calling methods that led to the failing method.
- Inspect the source code: Review the indicated line and the values involved in the operation.
13. Common Exception-Related Problems
| Exception |
Possible Cause |
Debugging Approach |
NullPointerException |
A method or field is accessed through a null reference. |
Inspect the reference, trace where it was assigned, and validate required objects. |
ArithmeticException |
Invalid arithmetic, such as integer division by zero. |
Inspect operands and validate the divisor before the operation. |
ArrayIndexOutOfBoundsException |
An array is accessed using an invalid index. |
Check the index against the array's valid range. |
NumberFormatException |
A string cannot be parsed as the requested numeric type. |
Inspect the input and validate its format before parsing. |
FileNotFoundException |
A file path is incorrect or the file cannot be opened. |
Check the path, file existence, permissions, and runtime working directory. |
ClassCastException |
An object is cast to an incompatible type. |
Inspect the actual runtime type and revise the type-handling logic. |
IllegalArgumentException |
A method receives an argument that violates its requirements. |
Inspect the supplied value and the method's validation rules. |
14. Debugging with Print Statements
Print statements can help developers inspect variable values and determine how far the program has executed. This technique is simple and can be useful for small examples or quick checks.
public class DebugPrintExample {
public static void main(String[] args) {
int numerator = 100;
int denominator = 0;
System.out.println("Numerator: " + numerator);
System.out.println("Denominator: " + denominator);
if (denominator == 0) {
System.out.println("Debug: denominator is zero.");
return;
}
System.out.println(numerator / denominator);
}
}
Explanation: The print statements display the operand values. The conditional check identifies the invalid denominator before the division occurs.
Limitation: Excessive print statements can clutter output and may expose sensitive information. For complex applications, use a debugger or an appropriate logging framework.
15. Debugging Exceptions Using an IDE
Java IDEs such as IntelliJ IDEA, Eclipse, and Visual Studio Code with Java extensions provide debugging tools that allow developers to pause execution and inspect program state.
Step-by-Step Debugging Process
- Open the Java source file containing the suspected problem.
- Place a breakpoint on the line where the failure may occur.
- Start the program in debug mode.
- Reproduce the input or action that triggers the problem.
- Inspect local variables and method parameters.
- Use step-over, step-into, and step-out commands to follow execution.
- Check the call stack to understand how the current method was reached.
- Inspect the exception message and cause when the debugger pauses on an exception.
- Correct the underlying logic and run the program again.
Important Debugging Features
| Feature |
Purpose |
| Breakpoint |
Pauses execution at a chosen line. |
| Step Over |
Executes the current line without entering called methods. |
| Step Into |
Enters a called method to inspect its execution. |
| Step Out |
Continues until the current method returns. |
| Variables panel |
Displays current variable values. |
| Call stack |
Shows the active method-call sequence. |
| Exception breakpoint |
Can pause execution when a configured exception is thrown or remains uncaught, depending on IDE settings. |
16. Using Logging to Investigate Exceptions
Logging records information about an application's execution. Unlike temporary print statements, a logging framework can support log levels, timestamps, configurable output destinations, and structured diagnostic information.
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggingExample {
private static final Logger LOGGER =
Logger.getLogger(LoggingExample.class.getName());
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
LOGGER.log(Level.SEVERE, "Calculation failed.", e);
}
}
}
Explanation: The logger records a severe-level message and the exception details, including the stack trace. Logging the exception object is generally more useful than logging only e.getMessage().
Logging Best Practices
- Use appropriate log levels such as INFO, WARNING, and SEVERE.
- Include meaningful context, such as the operation being performed.
- Avoid logging the same exception repeatedly at every layer.
- Never log passwords, access tokens, or unnecessary sensitive information.
- Preserve stack traces when they are needed for diagnosis.
- Use a consistent logging strategy across the application.
17. Debugging Exception Chaining
When one exception wraps another, inspect both the top-level exception and its cause. The top-level message may explain the failed business operation, while the cause identifies the lower-level technical problem.
try {
// Application operation
} catch (IllegalArgumentException e) {
throw new IllegalStateException(
"Unable to complete the requested operation.", e
);
}
In a real application, only wrap an exception when the new exception adds useful context or represents a meaningful abstraction. Do not replace an exception and discard its cause unless there is a deliberate reason to do so.
18. Common Debugging Mistakes
| Mistake |
Why It Is Harmful |
Better Approach |
| Ignoring the exception message |
Important clues about the failure may be missed. |
Read the exception type, message, and cause. |
| Inspecting only the last stack-trace line |
The most useful application frame may appear near the top. |
Examine the entire trace and identify relevant application frames. |
| Using an empty catch block |
The failure becomes difficult to detect and diagnose. |
Recover, report, or propagate the exception deliberately. |
| Fixing only the visible symptom |
The underlying problem may occur again. |
Trace the source of the invalid state or input. |
| Logging only a generic message |
Diagnostic details may be lost. |
Include useful context and the exception object when appropriate. |
| Using overly broad exception handling |
Unexpected failures may be handled incorrectly. |
Catch specific exception types where possible. |
| Reproducing the problem with different inputs |
The original failure may be difficult to compare. |
Record and repeat the relevant input and conditions. |
| Changing code without retesting |
The fix may introduce a new problem. |
Run targeted tests and regression tests. |
19. Best Practices for Custom Exceptions
- Choose meaningful class names ending in
Exception when appropriate.
- Use checked exceptions when callers should explicitly handle a condition and unchecked exceptions for suitable programming-contract violations.
- Write clear messages that describe the failure without exposing sensitive information.
- Provide a constructor accepting a cause when exception chaining is useful.
- Keep custom exceptions focused on a meaningful application condition.
- Avoid creating a new exception class when a standard Java exception already describes the problem well.
- Do not use exceptions for routine conditions that are better handled with ordinary control flow.
- Catch exceptions at a layer that can recover, add meaningful context, or report the failure appropriately.
- Preserve the original cause when translating exceptions.
- Test both successful operations and failure scenarios.
20. Practical Exercise: Student Marks Validation
Objective: Create a custom exception that rejects marks outside the range of 0 to 100, and debug invalid input conditions.
class InvalidMarksException extends Exception {
public InvalidMarksException(String message) {
super(message);
}
}
public class StudentMarksValidator {
static void validateMarks(int marks)
throws InvalidMarksException {
if (marks < 0 || marks > 100) {
throw new InvalidMarksException(
"Marks must be between 0 and 100."
);
}
System.out.println("Valid marks: " + marks);
}
public static void main(String[] args) {
int marks = 120;
try {
validateMarks(marks);
} catch (InvalidMarksException e) {
System.out.println("Validation failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Expected Behavior
The program rejects the value 120 because it is outside the permitted range. It displays a descriptive message and prints the stack trace to help identify where the exception was thrown.
Tasks
- Change the marks value to 85 and verify that validation succeeds.
- Test values below zero and above 100.
- Set a breakpoint on the condition that checks the marks.
- Inspect the value of
marks in the debugger.
- Remove the catch block temporarily and observe how the checked exception requirement affects compilation.
- Add a constructor that accepts a cause and practice exception chaining.
21. Mini Project: Product Price Validation and Debugging
Project goal: Build a product price validator that rejects invalid prices and provides useful diagnostic information.
class InvalidProductPriceException extends Exception {
public InvalidProductPriceException(String message) {
super(message);
}
public InvalidProductPriceException(
String message, Throwable cause) {
super(message, cause);
}
}
public class ProductPriceValidator {
static void validatePrice(double price)
throws InvalidProductPriceException {
if (!Double.isFinite(price) || price <= 0) {
throw new InvalidProductPriceException(
"Product price must be a finite positive number."
);
}
System.out.println("Valid product price: " + price);
}
public static void main(String[] args) {
double price = -50.0;
try {
validatePrice(price);
} catch (InvalidProductPriceException e) {
System.out.println("Validation error: " + e.getMessage());
}
}
}
Project Requirements
- Create a checked custom exception for invalid product prices.
- Reject zero, negative, NaN, and infinite price values.
- Display a meaningful validation message.
- Use an IDE breakpoint to inspect the price value.
- Test valid and invalid inputs.
- Write a short explanation of how the exception helps separate validation logic from error reporting.
22. Interview Questions and Answers
Q1. What is a custom exception in Java?
Answer: A custom exception is a user-defined exception class created to represent a specific application or business failure that is not clearly represented by an existing exception type.
Q2. How do you create a checked custom exception?
Answer: Create a class that extends Exception or another checked exception class, and add constructors as needed.
Q3. How do you create an unchecked custom exception?
Answer: Extend RuntimeException. The compiler will not require callers to catch or declare that exception.
Q4. What is the purpose of super(message)?
Answer: It passes the supplied message to the parent exception constructor so that it can be retrieved through methods such as getMessage().
Q5. What is exception chaining?
Answer: Exception chaining preserves an underlying exception as the cause of a higher-level exception, helping developers understand both the application-level failure and its technical origin.
Q6. What is a stack trace?
Answer: A stack trace shows the sequence of method calls active when an exception was thrown. It helps identify the location of the failure and the calls that led to it.
Q7. How can you debug a NullPointerException?
Answer: Read the stack trace, locate the relevant source line, inspect the reference and its assignment path, and determine why the object is null before it is accessed.
Q8. What is the difference between throw and throws?
Answer: throw throws an exception object, while throws declares throwable types that a method may propagate to its caller.
Q9. Why should the original exception cause be preserved?
Answer: The original cause provides lower-level diagnostic details that may be necessary to identify the real source of a failure.
Q10. What is the purpose of a breakpoint?
Answer: A breakpoint pauses program execution at a selected location so the developer can inspect variables, follow the call stack, and investigate the program's state.
Q11. Should every exception be caught immediately?
Answer: No. An exception should be caught where meaningful recovery or handling is possible. Otherwise, it may be propagated to a caller that can handle it appropriately.
Q12. Why is an empty catch block discouraged?
Answer: It silently ignores the failure, making debugging and diagnosis difficult and potentially allowing the application to continue in an invalid state.
23. Quick Revision Checklist
- A custom exception represents an application-specific failure.
- Extending
Exception creates a checked exception when it does not inherit from an unchecked type.
- Extending
RuntimeException creates an unchecked exception.
super(message) passes a message to the parent exception class.
throw throws an exception object.
throws declares throwable types in a method signature.
- Exception chaining preserves the original cause.
- A stack trace helps locate the origin of a failure.
- Breakpoints and variable inspection help identify incorrect program state.
- Logging should include useful diagnostic details without exposing sensitive data.
- Catch exceptions where they can be handled meaningfully.
- Always retest the program after correcting an exception-related problem.
24. Conclusion
Creating custom exceptions allows Java developers to express application-specific failures clearly and consistently. Choosing between checked and unchecked exceptions, providing meaningful constructors, and preserving causes helps build maintainable exception-handling logic.
Debugging is equally important. By understanding exception messages, reading stack traces, inspecting variables, using IDE debugging tools, and applying appropriate logging, developers can find the root cause of failures instead of simply hiding their symptoms.
To continue learning Java programming and exception handling, explore the Core Java Training Course, register for a course demo, or visit the JustAcademy website.