Chapter Introduction
Welcome to Chapter 12 of our Java project series! In this chapter, we pivot our focus from merely making our applications functional to making them resilient and user-friendly. We will dive deep into the critical aspects of robust error handling and meticulous input validation. While our previous projects demonstrated core logic, they often assumed perfect user input and didn’t gracefully handle unexpected situations.
This step is paramount for any production-ready application. Without proper error handling, an application can crash unexpectedly, provide incorrect results, or even expose sensitive information. Input validation, on the other hand, acts as the first line of defense, ensuring that only valid and safe data enters our system. By the end of this chapter, you will understand how to anticipate potential issues, guide users with clear feedback, and maintain application stability.
As a prerequisite, we will be building upon the SimpleCalculator project, which you should have completed in an earlier chapter. If you haven’t, please ensure you have a basic console-based calculator that can perform addition, subtraction, multiplication, and division. Our expected outcome for this chapter is a SimpleCalculator that can robustly handle various invalid inputs, such as non-numeric values, unsupported operators, and the dreaded division by zero, all while providing clear, actionable feedback to the user and logging internal errors for developers.
Planning & Design
Robust error handling and input validation require thoughtful planning. We need to identify potential points of failure and design mechanisms to gracefully manage them.
Identifying Error Types
For our SimpleCalculator, we can foresee several types of errors:
- Invalid Input Format:
- User enters text instead of numbers (e.g., “hello” for operand).
- User enters an invalid operator (e.g., “&”, “mod”).
- Logical Errors:
- Division by zero.
- System Errors: (Less likely in a simple console app, but good to consider for larger projects)
- Out of memory.
- File I/O errors (if we were reading from a file).
Design Principles for Error Handling
- Fail Fast: Validate input as early as possible.
- Specific Exceptions: Use custom exceptions for business-logic errors to provide clarity.
- Graceful Recovery: Allow the application to continue running or prompt the user for correct input rather than crashing.
- User-Friendly Messages: Present clear, non-technical error messages to the end-user.
- Detailed Logging: Log comprehensive technical details for developers to aid in debugging and monitoring.
- Centralized Handling: Where appropriate, centralize error handling logic to avoid repetition.
Component Architecture for Error Handling
We will modify our existing SimpleCalculator class.
SimpleCalculatorClass: Will contain the core calculation logic.CalculatorAppClass: Will handle user interaction, input reading, validation, and exception catching.- Custom Exception Classes: We’ll introduce specific exception types for
InvalidInputExceptionandDivisionByZeroException. - Logging Framework: We’ll integrate SLF4J with Logback for structured logging.
File Structure
Our project structure will look something like this:
simple-calculator/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── calculator/
│ │ │ ├── Calculator.java
│ │ │ ├── CalculatorApp.java
│ │ │ ├── exception/
│ │ │ │ ├── InvalidInputException.java
│ │ │ │ └── DivisionByZeroException.java
│ │ │ └── util/
│ │ │ └── InputValidator.java
│ │ └── resources/
│ │ └── logback.xml
├── pom.xml
Step-by-Step Implementation
Let’s begin by setting up our project and then incrementally adding error handling and validation.
a) Setup/Configuration
First, we need to add logging dependencies to our pom.xml. We’ll use SLF4J as the logging facade and Logback as the implementation.
Dependencies to Install:
Open your pom.xml file and add the following dependencies within the <dependencies> block.
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.calculator</groupId>
<artifactId>simple-calculator</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source> <!-- Targeting Java 21 LTS for stability and modern features -->
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>2.0.12</slf4j.version> <!-- Latest stable as of Dec 2025 -->
<logback.version>1.4.14</logback.version> <!-- Latest stable as of Dec 2025 -->
</properties>
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Logback Classic (SLF4J implementation) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- Logback Core (dependency for logback-classic) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- JUnit for testing (if not already present) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.0-M1</version> <!-- Latest stable as of Dec 2025 -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.0-M1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.example.calculator.CalculatorApp</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Why these versions? As of December 2025, Java 21 is the latest Long-Term Support (LTS) release, making it a robust and stable choice for production applications. While Java 24/25 are available, LTS versions are often preferred for long-term projects. SLF4J 2.x and Logback 1.4.x are the latest stable versions compatible with modern Java.
Configuration for Logging:
Create a logback.xml file in src/main/resources. This configures Logback to print messages to the console and to a file.
File: src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console appender -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- File appender -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/calculator.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Daily rollover -->
<fileNamePattern>logs/calculator-%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- Keep 30 days of history -->
<maxHistory>30</maxHistory>
<!-- Max file size 10MB -->
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Root logger configuration -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
<!-- Specific logger for our application package -->
<logger name="com.example.calculator" level="DEBUG" />
</configuration>
Why this configuration?
STDOUTAppender: Sends logs to the console, useful for immediate feedback during development.FILEAppender: Writes logs to acalculator.logfile.RollingFileAppenderwithTimeBasedRollingPolicy: This is a best practice for production. It ensures that log files don’t grow indefinitely, rolling over daily and keeping a specified history (30 days here). This prevents disk space exhaustion and makes log management easier.- Pattern: Includes timestamp, thread, log level, logger name, and message, providing rich context.
- Root Level
INFO: Default level for all loggers. OnlyINFO,WARN,ERRORmessages are shown. com.example.calculatorLevelDEBUG: We set a more verbose level for our application’s package, allowing us to seeDEBUGmessages during development without cluttering the console/file withDEBUGmessages from third-party libraries.
b) Core Implementation
Let’s start by defining our custom exception classes.
File: src/main/java/com/example/calculator/exception/InvalidInputException.java
package com.example.calculator.exception;
/**
* Custom exception for invalid user input.
* This can cover non-numeric input, invalid operators, etc.
*/
public class InvalidInputException extends RuntimeException {
public InvalidInputException(String message) {
super(message);
}
public InvalidInputException(String message, Throwable cause) {
super(message, cause);
}
}
Why a custom RuntimeException?
- Clarity:
InvalidInputExceptionis much more descriptive than a genericIllegalArgumentExceptionorNumberFormatException. It clearly indicates that the problem stems from the user’s input. - Business Logic: It allows us to differentiate between problems with the application’s internal logic and problems caused by external factors (like user input).
RuntimeException: For user input errors, it’s often acceptable to useRuntimeExceptionbecause these are typically unrecoverable by the method itself and should be handled at a higher level (e.g., in the UI/console loop). This avoids cluttering method signatures with checked exceptions.
File: src/main/java/com/example/calculator/exception/DivisionByZeroException.java
package com.example.calculator.exception;
/**
* Custom exception specifically for division by zero errors.
*/
public class DivisionByZeroException extends RuntimeException {
public DivisionByZeroException(String message) {
super(message);
}
public DivisionByZeroException(String message, Throwable cause) {
super(message, cause);
}
}
Why a separate DivisionByZeroException?
- Specificity: Division by zero is a very specific type of mathematical error. Having a dedicated exception allows for more precise handling and clearer error messages to the user.
- Semantic Value: It communicates the exact nature of the problem, which is valuable for both developers debugging and users receiving feedback.
Next, let’s create an InputValidator utility class to centralize our validation logic. This promotes reusability and clean code.
File: src/main/java/com/example/calculator/util/InputValidator.java
package com.example.calculator.util;
import com.example.calculator.exception.InvalidInputException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class for validating user input.
* Centralizes validation logic to promote reusability and maintainability.
*/
public class InputValidator {
private static final Logger logger = LoggerFactory.getLogger(InputValidator.class);
private static final String VALID_OPERATORS = "+-*/";
private InputValidator() {
// Private constructor to prevent instantiation of utility class
}
/**
* Validates if a given string can be parsed into a double.
*
* @param input The string to validate.
* @return The parsed double value.
* @throws InvalidInputException if the input is not a valid number.
*/
public static double validateAndParseDouble(String input, String fieldName) {
logger.debug("Validating and parsing input for {}: {}", fieldName, input);
try {
return Double.parseDouble(input);
} catch (NumberFormatException e) {
logger.warn("Invalid numeric input for {}: '{}'", fieldName, input, e);
throw new InvalidInputException(
String.format("Invalid numeric input for %s. Please enter a valid number.", fieldName), e);
}
}
/**
* Validates if a given string is a supported arithmetic operator.
*
* @param operator The operator string to validate.
* @return The validated operator string.
* @throws InvalidInputException if the operator is not supported.
*/
public static String validateOperator(String operator) {
logger.debug("Validating operator: {}", operator);
if (operator == null || operator.trim().isEmpty()) {
logger.warn("Operator cannot be empty.");
throw new InvalidInputException("Operator cannot be empty. Please enter +, -, *, or /.");
}
String trimmedOperator = operator.trim();
if (trimmedOperator.length() != 1 || !VALID_OPERATORS.contains(trimmedOperator)) {
logger.warn("Invalid operator: '{}'", trimmedOperator);
throw new InvalidInputException(
String.format("Invalid operator '%s'. Please use one of: %s", trimmedOperator, VALID_OPERATORS));
}
return trimmedOperator;
}
}
Explanation:
logger: An instance oforg.slf4j.Loggeris used for logging. This allows us to logDEBUGmessages during validation,WARNmessages when validation fails, and potentiallyERRORmessages for more critical issues.validateAndParseDouble: This method attempts to parse a string into adouble. IfNumberFormatExceptionoccurs, it catches it, logs a warning, and re-throws our customInvalidInputExceptionwith a user-friendly message.validateOperator: This method checks if the provided operator is one of the supported ones (+,-,*,/). It also handles empty or null input.Private Constructor: Prevents accidental instantiation of this utility class.
Now, let’s update our Calculator class to incorporate the new exceptions and validation.
File: src/main/java/com/example/calculator/Calculator.java
package com.example.calculator;
import com.example.calculator.exception.DivisionByZeroException;
import com.example.calculator.exception.InvalidInputException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides core arithmetic operations with robust error handling.
*/
public class Calculator {
private static final Logger logger = LoggerFactory.getLogger(Calculator.class);
/**
* Performs a specified arithmetic operation on two numbers.
*
* @param operand1 The first operand.
* @param operand2 The second operand.
* @param operator The arithmetic operator (+, -, *, /).
* @return The result of the operation.
* @throws DivisionByZeroException if the operator is '/' and operand2 is zero.
* @throws InvalidInputException if an unsupported operator is provided.
*/
public double calculate(double operand1, double operand2, String operator) {
logger.debug("Performing calculation: {} {} {}", operand1, operator, operand2);
double result;
switch (operator) {
case "+":
result = operand1 + operand2;
break;
case "-":
result = operand1 - operand2;
break;
case "*":
result = operand1 * operand2;
break;
case "/":
if (operand2 == 0) {
logger.error("Attempted division by zero: {} / {}", operand1, operand2);
throw new DivisionByZeroException("Cannot divide by zero.");
}
result = operand1 / operand2;
break;
default:
logger.error("Unsupported operator encountered: '{}'", operator);
// This case should ideally be caught by InputValidator, but included for defensive programming
throw new InvalidInputException(
String.format("Unsupported operator: '%s'. Please use +, -, *, or /.", operator));
}
logger.info("Calculation result: {} {} {} = {}", operand1, operator, operand2, result);
return result;
}
}
Changes and Explanation:
logger: Integrated SLF4J logger for internal logging.DivisionByZeroException: Explicitly thrown whenoperand2is 0 during division. This is a specific business logic error.InvalidInputException: Thrown if anoperatorsomehow bypasses theInputValidator(defensive programming).- Logging:
DEBUGfor method entry,ERRORfor exceptions,INFOfor successful calculations. This helps track the flow and pinpoint issues.
Finally, we’ll create or modify the CalculatorApp class, which is our application’s entry point, to handle user interaction, validation, and gracefully catch exceptions.
File: src/main/java/com/example/calculator/CalculatorApp.java
package com.example.calculator;
import com.example.calculator.exception.DivisionByZeroException;
import com.example.calculator.exception.InvalidInputException;
import com.example.calculator.util.InputValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.InputMismatchException;
import java.util.Scanner;
/**
* Main application class for the Robust Simple Calculator.
* Handles user input, validation, calculation, and error display.
*/
public class CalculatorApp {
private static final Logger logger = LoggerFactory.getLogger(CalculatorApp.class);
private final Calculator calculator;
private final Scanner scanner;
public CalculatorApp() {
this.calculator = new Calculator();
this.scanner = new Scanner(System.in);
}
public static void main(String[] args) {
logger.info("Starting Simple Calculator application...");
CalculatorApp app = new CalculatorApp();
app.run();
logger.info("Simple Calculator application stopped.");
}
public void run() {
boolean running = true;
while (running) {
try {
System.out.println("\nEnter first number (or 'exit' to quit):");
String input1 = scanner.nextLine();
if ("exit".equalsIgnoreCase(input1)) {
running = false;
continue;
}
double operand1 = InputValidator.validateAndParseDouble(input1, "first number");
System.out.println("Enter operator (+, -, *, /):");
String operator = scanner.nextLine();
operator = InputValidator.validateOperator(operator); // Validate operator using utility
System.out.println("Enter second number:");
String input2 = scanner.nextLine();
double operand2 = InputValidator.validateAndParseDouble(input2, "second number");
double result = calculator.calculate(operand1, operand2, operator);
System.out.printf("Result: %.2f %s %.2f = %.2f%n", operand1, operator, operand2, result);
} catch (InvalidInputException e) {
// Catch custom validation errors
logger.warn("User input error: {}", e.getMessage());
System.err.println("Error: " + e.getMessage());
} catch (DivisionByZeroException e) {
// Catch specific division by zero error
logger.error("Arithmetic error: {}", e.getMessage());
System.err.println("Error: " + e.getMessage());
} catch (InputMismatchException e) {
// Catch unexpected scanner input issues (less likely with nextLine, but good for robustness)
logger.error("Unexpected input format from scanner.", e);
System.err.println("An unexpected input format was detected. Please try again.");
scanner.next(); // Consume the invalid token to prevent infinite loop
} catch (Exception e) {
// Catch any other unexpected runtime exceptions
logger.error("An unexpected error occurred during calculation.", e);
System.err.println("An unexpected internal error occurred. Please try again or contact support.");
}
System.out.println("------------------------------------");
}
scanner.close(); // Close the scanner when done
}
}
Changes and Explanation:
logger: Integrated SLF4J logger for application-level events.run()Method: Encapsulates the main application loop.- Continuous Loop: The
while(running)loop allows the user to perform multiple calculations or enter “exit” to quit. InputValidatorUsage:InputValidator.validateAndParseDouble()andInputValidator.validateOperator()are used to validate user input before passing it to theCalculator.try-catchBlock: This is the core of our error handling.InvalidInputException: Catches validation errors, logs aWARNmessage (user error, not a bug in our code), and prints a user-friendly error toSystem.err.DivisionByZeroException: Catches the specific arithmetic error, logs anERRORmessage, and provides targeted feedback.InputMismatchException: Althoughscanner.nextLine()is used, which typically consumes the entire line, including this for robustness against potential future changes or unusual console behaviors is good practice. It also demonstrates how to recover by consuming the bad token.Exception e: A general catch-all for any other unexpected runtime errors. This is crucial for production applications to prevent crashes and provide a fallback error message, while logging the full stack trace for developers.
scanner.close(): Important to close resources to prevent resource leaks.- User Feedback:
System.err.printlnis used for error messages, clearly distinguishing them from normal output.
c) Testing This Component
Now that we’ve implemented robust error handling and input validation, let’s test it thoroughly.
To run the application:
- Open your terminal or command prompt.
- Navigate to the
simple-calculatorproject root directory. - Compile and run the application using Maven:
mvn clean install java -jar target/simple-calculator-1.0-SNAPSHOT.jar
Expected Behavior & Test Cases:
Valid Input:
- Enter
10,+,5. Expected:Result: 10.00 + 5.00 = 15.00 - Enter
7.5,*,2. Expected:Result: 7.50 * 2.00 = 15.00 - Check
logs/calculator.logforINFOmessages about successful calculations.
- Enter
Invalid Numeric Input (First Operand):
- Enter
abcfor the first number. - Expected:
Error: Invalid numeric input for first number. Please enter a valid number. - Check
logs/calculator.logforWARNmessages related toInvalid numeric input.
- Enter
Invalid Numeric Input (Second Operand):
- Enter
10,+,xyz. - Expected:
Error: Invalid numeric input for second number. Please enter a valid number. - Check
logs/calculator.logforWARNmessages related toInvalid numeric input.
- Enter
Invalid Operator:
- Enter
10,&,5. - Expected:
Error: Invalid operator '&'. Please use one of: +-*/ - Check
logs/calculator.logforWARNmessages related toInvalid operator.
- Enter
Empty Operator:
- Enter
10, (press Enter for empty operator),5. - Expected:
Error: Operator cannot be empty. Please enter +, -, *, or /. - Check
logs/calculator.logforWARNmessages related toOperator cannot be empty.
- Enter
Division by Zero:
- Enter
10,/,0. - Expected:
Error: Cannot divide by zero. - Check
logs/calculator.logforERRORmessages related toAttempted division by zero.
- Enter
Exit Command:
- Enter
exitfor the first number. - Expected: Application quits gracefully.
- Check
logs/calculator.logfor “Simple Calculator application stopped.”INFOmessage.
- Enter
Debugging Tips:
- Check Console Output: Pay close attention to the
System.errmessages for user feedback andSystem.outfor normal output. - Review
logs/calculator.log: This file is your best friend. Look forDEBUGmessages to trace execution flow,WARNmessages for validation failures, andERRORmessages for exceptions. The stack traces in the log file are invaluable for pinpointing the exact line of code where an error originated. - Step-Through Debugger: If you’re using an IDE like IntelliJ IDEA or Eclipse, set breakpoints in
InputValidator,Calculator, andCalculatorApp’stry-catchblocks. Step through the code with various inputs to observe variable values and execution paths.
Production Considerations
When deploying applications with robust error handling, several production considerations come into play:
- Centralized Error Reporting: For larger applications, integrate with an error monitoring service (e.g., Sentry, Bugsnag, Datadog) that can automatically collect and aggregate error logs, notify developers, and provide context (user, environment, stack trace).
- Security of Error Messages: Never expose raw stack traces or sensitive system information directly to end-users. User-facing error messages should be generic and helpful (e.g., “An unexpected error occurred”), while detailed technical errors are logged securely. Our current setup follows this by printing user-friendly messages to
System.errand full details to the log file. - Performance of Logging: Excessive
DEBUGlevel logging in production can impact performance and consume significant disk space. Use logging levels judiciously. Ourlogback.xmlsets the root level toINFOfor this reason, while allowingDEBUGfor our specific package during development. For production, you might setcom.example.calculatortoINFOorWARN. - Log Rotation and Retention: As configured in
logback.xml, ensure log files are rotated (e.g., daily) and old logs are automatically purged. This prevents disk space exhaustion and complies with data retention policies. - Monitoring and Alerts: Set up monitoring tools to alert operations teams when
ERRORlevel logs appear at an unusual rate, indicating potential system issues.
Code Review Checkpoint
At this point, we have significantly enhanced the robustness of our SimpleCalculator.
Summary of what was built:
- Introduced two custom
RuntimeExceptionclasses:InvalidInputExceptionandDivisionByZeroExceptionfor clear error semantics. - Created an
InputValidatorutility class to centralize and reuse input validation logic. - Modified the
Calculatorclass to throw specific exceptions for division by zero and unsupported operations. - Refactored
CalculatorAppto handle user input in a continuous loop, validate input usingInputValidator, and gracefully catch various exceptions (InvalidInputException,DivisionByZeroException,InputMismatchException, and genericException). - Integrated SLF4J and Logback for comprehensive, production-ready logging, including console and file appenders with daily rolling.
Files Created/Modified:
pom.xml(added logging dependencies)src/main/resources/logback.xml(new file for logging configuration)src/main/java/com/example/calculator/exception/InvalidInputException.java(new file)src/main/java/com/example/calculator/exception/DivisionByZeroException.java(new file)src/main/java/com/example/calculator/util/InputValidator.java(new file)src/main/java/com/example/calculator/Calculator.java(modified for logging and throwing exceptions)src/main/java/com/example/calculator/CalculatorApp.java(modified for input handling, validation, and exception catching)
How it integrates with existing code:
The changes are largely contained within the Calculator and CalculatorApp classes, with new utility and exception classes supporting them. The core arithmetic logic of Calculator remains, but it now signals errors more explicitly. CalculatorApp now acts as a resilient shell around the calculation logic, ensuring a stable user experience.
Common Issues & Solutions
Issue:
java.lang.ClassNotFoundException: org.slf4j.LoggerFactoryorNoClassDefFoundErrorfor Logback classes.- Cause: Logging dependencies are not correctly added to
pom.xmlor Maven hasn’t downloaded them. - Solution:
- Ensure
slf4j-api,logback-classic, andlogback-coredependencies are correctly listed in yourpom.xml. - Run
mvn clean installto download dependencies and rebuild the project. - Verify the
target/simple-calculator-1.0-SNAPSHOT.jarcontains the necessary JARs if you’re running it directly (thoughmvn installshould handle this forjava -jar).
- Ensure
- Cause: Logging dependencies are not correctly added to
Issue:
logback.xmlnot found or logging not working as expected (e.g., no file output).- Cause: The
logback.xmlfile is not in the correct location (src/main/resources) or has syntax errors. - Solution:
- Double-check the file path:
src/main/resources/logback.xml. - Validate the XML syntax.
- Ensure your
pom.xml’s<build>section correctly includessrc/main/resourcesin the build path (Maven does this by default, but custom configurations might override it). - Temporarily set
rootlogger level toDEBUGinlogback.xmlto see if Logback itself is logging initialization messages.
- Double-check the file path:
- Cause: The
Issue: Application crashes with
NumberFormatExceptiondespiteInputValidator.- Cause: This usually means you’re trying to parse input before it gets to
InputValidator.validateAndParseDouble(), or theInputValidatorisn’t being called for a particular input. - Solution:
- Carefully trace the execution flow in
CalculatorApp. Ensure all user inputs that are expected to be numbers are passed throughInputValidator.validateAndParseDouble(). - Check for any
Double.parseDouble()calls outside of theInputValidatorclass. - If debugging, verify that the
try-catch (InvalidInputException e)block is actually reached when invalid input is provided.
- Carefully trace the execution flow in
- Cause: This usually means you’re trying to parse input before it gets to
Testing & Verification
To verify all the changes made in this chapter, follow these steps:
Rebuild the Project:
mvn clean installThis ensures all new classes and
pom.xmlchanges are compiled and packaged.Run the Application:
java -jar target/simple-calculator-1.0-SNAPSHOT.jarExecute Test Scenarios: Go through the “Testing This Component” section again, covering all valid and invalid input cases.
- Valid calculations: Confirm correct results for
+,-,*,/with valid numbers (e.g.,10 + 5,15 - 3,4 * 6,20 / 4). - Non-numeric input: Enter text for numbers (e.g., “abc”, “hello”). Verify
Error: Invalid numeric input...messages. - Invalid operators: Enter unsupported symbols (e.g., “&”, “mod”, “add”). Verify
Error: Invalid operator...messages. - Empty operator: Press Enter without typing an operator. Verify
Error: Operator cannot be empty...message. - Division by zero: Enter
10 / 0. VerifyError: Cannot divide by zero.message. - Exit command: Type
exitto gracefully close the application.
- Valid calculations: Confirm correct results for
Inspect Log Files:
- Open
logs/calculator.log. - Verify that
INFOmessages appear for successful calculations and application start/stop. - Check for
WARNmessages whenInvalidInputExceptionoccurs (e.g., invalid numbers or operators). - Look for
ERRORmessages whenDivisionByZeroExceptionoccurs or any unexpectedExceptionis caught. - Ensure the log messages contain the correct timestamps, levels, and detailed information including stack traces for
ERRORlevel entries.
- Open
By performing these steps, you can confidently verify that our SimpleCalculator now handles various error conditions gracefully, provides clear feedback to the user, and logs detailed information for developers, making it much more robust and production-ready.
Summary & Next Steps
In this comprehensive chapter, we transformed our basic SimpleCalculator into a resilient application by implementing robust error handling and input validation. We learned the importance of anticipating user mistakes and system failures, designing custom exceptions for clarity, centralizing validation logic in utility classes, and leveraging a professional logging framework (SLF4J + Logback) for debugging and monitoring. We also covered critical production considerations to ensure our application behaves reliably and securely in real-world scenarios.
This foundation of error handling and validation is crucial for any application, regardless of its complexity. It ensures a stable user experience and provides developers with the necessary tools to diagnose and resolve issues efficiently.
In the next chapter, Chapter 13: Building the Number Guessing Game - Core Logic, we will apply these principles as we embark on building our next project: the Number Guessing Game. We will focus on implementing the game’s core logic, generating random numbers, and guiding the user through the guessing process, while keeping in mind the lessons learned about robust input handling and error management.