Java Exceptions for Validation and Safe Cleanup
Learn how to model validation failures with exceptions, preserve debugging causes, and close resources safely with try-with-resources.
Before this: Java 21 First Console App: Structure, Compile, Run
What you will learn
- After this lesson you can distinguish checked exceptions, unchecked exceptions, and errors in practical Java code.
- After this lesson you can use
try,catch,finally,throw,throws, and multi-catch intentionally. - After this lesson you can use try-with-resources to close
AutoCloseableresources such asScanner. - After this lesson you can create a meaningful custom exception and preserve the original cause.
1. The practical exception categories
Java exception handling is easier when you separate intent: - Checked exceptions: exceptions that Java requires callers to catch or declare. They often fit recoverable failures in APIs, such as invalid imported data. - Unchecked exceptions: RuntimeException and its subclasses. They commonly represent programming-contract failures, such as illegal arguments or invalid state. - Errors: serious JVM or environment failures that application code normally should not catch.
For validation, ask: is this bad external data that this API expects callers to report or recover from, or is it a bug in how the method was called? That decision drives whether a checked domain exception or an unchecked exception is clearer.
2. Throw domain exceptions, not vague strings
A custom exception gives the caller a precise type to catch. OrderFormatException is clearer than generic Exception because it says the failure belongs to order parsing, not to every possible operation.
Use throw at the point where you detect the invalid condition. Use throws on the method signature when the caller must handle or propagate that checked exception. Keep messages short but actionable: quantity must be positive is better than bad input.
3. Preserve causes for debugging
When low-level code throws an exception, do not hide it. If Integer.parseInt throws NumberFormatException, wrap it with your domain exception and pass the original exception as the cause.
This gives users a clean domain message while still giving developers the stack trace and root exception type. Losing the cause is a common reason production bugs become hard to diagnose.
4. Clean up with try-with-resources
Resources such as scanners, streams, sockets, and readers need cleanup. A finally block can do this manually, but try-with-resources is shorter and safer for anything implementing AutoCloseable.
In the example, Scanner is created in the try (...) header. Java closes it automatically when the block exits, whether the loop finishes normally or an exception is thrown.
5. Catch only what you can handle
The example catches OrderFormatException because the program can report the bad line and continue with the next one. It does not catch Error, and it does not use a broad catch (Exception ex).
Multi-catch is useful when different exception types have the same recovery behavior, for example catch (NumberFormatException | ArithmeticException ex). If the actions differ, use separate catch blocks so the code stays honest.
Code
import java.io.StringReader;
import java.util.Scanner;
public class Main {
static class OrderFormatException extends Exception {
OrderFormatException(String message) { super(message); }
OrderFormatException(String message, Throwable cause) { super(message, cause); }
}
record Order(String id, int quantity) {}
public static void main(String[] args) {
String input = "A-100,3\n ,2\nB-200,0\nC-300,two\nbad-line\nD-400,5";
try (Scanner scanner = new Scanner(new StringReader(input))) {
int lineNo = 0;
while (scanner.hasNextLine()) {
lineNo++;
String line = scanner.nextLine();
try {
Order order = parseOrder(line);
System.out.println("accepted " + order.id() + " x" + order.quantity());
} catch (OrderFormatException ex) {
System.out.println("line " + lineNo + ": " + ex.getMessage());
if (ex.getCause() != null) {
System.out.println(" cause: " + ex.getCause().getClass().getSimpleName());
}
}
}
}
}
static Order parseOrder(String line) throws OrderFormatException {
String[] parts = line.split(",", -1);
if (parts.length != 2) {
throw new OrderFormatException("expected id,quantity");
}
String id = parts[0].trim();
if (id.isEmpty()) {
throw new OrderFormatException("id is required");
}
int quantity;
try {
quantity = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException ex) {
throw new OrderFormatException("quantity must be an integer", ex);
}
if (quantity <= 0) {
throw new OrderFormatException("quantity must be positive");
}
return new Order(id, quantity);
}
}
accepted A-100 x3 line 2: id is required line 3: quantity must be positive line 4: quantity must be an integer cause: NumberFormatException line 5: expected id,quantity accepted D-400 x5
The program treats malformed order lines as a checked domain exception, validates blank IDs, preserves the NumberFormatException cause when integer parsing fails, and uses try-with-resources to close the Scanner.
Scanner scanner = null;
try {
scanner = new Scanner(source);
// read and validate data
} finally {
if (scanner != null) {
scanner.close();
}
}This is the older manual cleanup shape. Prefer try-with-resources when possible because it is shorter and avoids forgetting cleanup on exceptional paths.
Common mistakes
✕ Catching
Exceptioneverywhere and continuing as if nothing happened.✓ Catch the smallest exception type you can handle, report enough context, and let unexpected failures propagate.
✕ Treating every validation failure as either always checked or always unchecked.
✓ Choose based on API intent: use a checked domain exception when callers are expected to handle recoverable bad data; use unchecked exceptions for programmer-contract violations.
✕ Wrapping an exception but losing the original cause.
✓ Use a constructor like
new MyException("message", cause)so stack traces show the real source.✕ Closing resources manually only on the success path.
✓ Use try-with-resources so Java closes the resource even when parsing or processing throws.
Your challenge
Complete an order parser that returns Order(id, quantity) for valid id,quantity text and throws OrderFormatException for invalid text. It must reject missing IDs, non-integer quantities, and non-positive quantities. Preserve the original NumberFormatException as the cause when integer parsing fails.
Interview questions
How do checked exceptions differ from unchecked exceptions in Java?
Checked exceptions must be caught or declared with throws at compile time. They are useful when an API wants callers to explicitly handle or propagate a recoverable condition. Unchecked exceptions extend RuntimeException and usually represent programming-contract violations or failures that most callers cannot reasonably recover from.
Why should custom exceptions preserve the original cause?
The cause keeps the original exception type, message, and stack trace available. Without it, wrapping an exception hides the real failure point and makes debugging much harder.
When would you use finally instead of try-with-resources?
Use try-with-resources for AutoCloseable cleanup. Use finally for cleanup actions that are not represented by an AutoCloseable, or for code that must run regardless of success or failure, such as restoring a flag.
Why is catch (Exception ex) usually a poor default?
It catches too much, often including failures the code cannot correctly handle. Narrow catch blocks document expected failures and avoid accidentally swallowing bugs.
Practice
- Modify the runnable program so blank quantities are rejected with a different message:
quantity is required. - Add a separate catch branch for
IllegalArgumentExceptioninmainand compare it with catchingOrderFormatException. - Rewrite a catch block using multi-catch for two exception types you intentionally handle the same way.
Quick quiz
Key takeaways
- Use checked exceptions when an API should force callers to handle or declare recoverable domain failures.
- Use unchecked exceptions for programming-contract violations, not routine external data problems.
- Wrap low-level exceptions with meaningful domain exceptions and preserve the cause.
- Prefer try-with-resources for
AutoCloseablecleanup. - Catch the narrowest exception type that your code can actually handle.
References
- Oracle Java Tutorials: Exceptions (docs.oracle.com)
- Oracle Java API: Throwable (docs.oracle.com)
- Oracle Java API: AutoCloseable (docs.oracle.com)
- Oracle Java API: Scanner (docs.oracle.com)
Related YouTube lessons
See the full Java learning path