Exception Handling

Project: Robust Input Handling

15 min Lesson 10 of 14

Project: Robust Input Handling

You have spent the last nine lessons learning every corner of Java exception handling — from the basics of try/catch all the way to custom exception classes and try-with-resources. This final lesson brings everything together in one realistic mini-project: a console application that keeps asking the user for input until they provide something valid, and signals bad input by throwing a custom exception.

Real applications rarely crash on bad input — they tell the user what went wrong and ask again. That is the pattern you will build here.

What We Are Building

A small command-line program that reads an integer between 1 and 100 from the user. If the user types a non-number, or a number outside that range, the program:

  1. Throws an InvalidInputException (a custom checked exception you define).
  2. Catches it, prints a friendly message, and loops back to prompt again.
  3. Exits the loop only when valid input is received.

Step 1 — Define the Custom Exception

From Lesson 7 you know that a custom exception is simply a class that extends Exception (checked) or RuntimeException (unchecked). Here we use a checked exception because the caller must handle bad input — it is an expected part of the flow.

public class InvalidInputException extends Exception { private final String userInput; public InvalidInputException(String message, String userInput) { super(message); this.userInput = userInput; } public String getUserInput() { return userInput; } }
Why store the raw input? Keeping the bad value inside the exception lets any catching code show the user exactly what they typed, without needing to pass it as a separate variable.

Step 2 — A Validator Method That Throws

Separate the validation logic into its own method. It accepts a raw String, tries to parse it, checks the range, and either returns a clean int or throws InvalidInputException. The throws declaration makes the contract explicit.

public static int parseAndValidate(String raw) throws InvalidInputException { int value; try { value = Integer.parseInt(raw.trim()); } catch (NumberFormatException e) { throw new InvalidInputException( "\"" + raw.trim() + "\" is not a whole number.", raw); } if (value < 1 || value > 100) { throw new InvalidInputException( value + " is out of range. Enter a number between 1 and 100.", raw); } return value; }
Wrap, do not swallow. Notice how NumberFormatException is caught and immediately re-thrown as InvalidInputException. The original low-level exception is absorbed, and a meaningful domain-level exception is raised in its place. The user sees "not a whole number", not a raw stack trace.

Step 3 — The Input Loop

The main loop runs indefinitely (while (true)) and breaks only when valid input arrives. On every iteration it prompts, reads a line, calls the validator, and either stores the result and breaks, or catches the exception and prints the error before looping.

import java.util.Scanner; public class RobustInputDemo { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int validNumber = 0; System.out.println("=== Robust Input Demo ==="); while (true) { System.out.print("Enter a number between 1 and 100: "); String line = scanner.nextLine(); try { validNumber = parseAndValidate(line); break; // only reached on success } catch (InvalidInputException e) { System.out.println(" [Error] " + e.getMessage()); System.out.println(" You typed: \"" + e.getUserInput() + "\""); System.out.println(" Please try again.\n"); } } System.out.println("Great! You entered: " + validNumber); scanner.close(); } // parseAndValidate() goes here (see Step 2) }

Sample Run

Here is what the program looks like when a user makes two mistakes before succeeding:

=== Robust Input Demo === Enter a number between 1 and 100: hello [Error] "hello" is not a whole number. You typed: "hello" Please try again. Enter a number between 1 and 100: 150 [Error] 150 is out of range. Enter a number between 1 and 100. You typed: "150" Please try again. Enter a number between 1 and 100: 42 Great! You entered: 42

Why This Design Works

  • Single responsibility: parseAndValidate handles all validation; main handles the loop and user interaction. Each piece does one job.
  • Checked exception forces handling: Because InvalidInputException extends Exception, the compiler forces every caller to either catch it or declare throws. You cannot accidentally ignore bad input.
  • No silent failures: The loop never exits until valid input is produced. There is no return -1 sentinel value to forget to check.
  • Informative error messages: The exception carries both a human-readable message and the raw input, so the catch block can give the user specific feedback.

Extending the Project (Ideas)

Now that the pattern is solid, try these small extensions on your own:

  • Add a maximum retry limit (e.g., three attempts), then throw a different exception if the limit is exceeded.
  • Validate that input is not blank before trying to parse it, and give a specific message for empty input.
  • Wrap the Scanner in a try-with-resources block (Lesson 8) so it is always closed — even if an unexpected exception escapes the loop.
  • Move the validator into a utility class and write a unit test that checks every error branch.
Never use exceptions for normal flow control. The retry loop here is the right pattern because bad input is genuinely exceptional — it is not the expected case. If you found yourself throwing exceptions to signal that a list is empty, or to break out of nested loops, that is a misuse of the mechanism and will confuse every reader of your code.

Putting It All Together

This project touched almost every concept from the tutorial: defining a custom exception class (Lesson 7), using throws in a method signature (Lesson 5), catching and re-throwing from a low-level exception (Lesson 6), and structuring a retry loop that gives the user meaningful feedback. Exception handling is not just about stopping crashes — it is about building programs that communicate clearly and recover gracefully. That is the skill you now have.