Skip to content
Tutorialintermediate · 2 min read · 2026-11-01

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 AutoCloseable resources such as Scanner.
  • 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

Java · Validated order parsing with custom exception and try-with-resources✓ Executed & verified · javac 21.0.13
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);
    }
}
Output (real run)
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.

Java · Manual cleanup pattern using finallyIllustrative · not executed
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 Exception everywhere 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

  1. Modify the runnable program so blank quantities are rejected with a different message: quantity is required.
  2. Add a separate catch branch for IllegalArgumentException in main and compare it with catching OrderFormatException.
  3. Rewrite a catch block using multi-catch for two exception types you intentionally handle the same way.

Quick quiz

Q1. In the runnable example, why can `OrderFormatException` reasonably be checked?
Q2. What is the main benefit of `throw new OrderFormatException("quantity must be an integer", ex)`?
Q3. Which statement best describes try-with-resources?
Q4. When is multi-catch most appropriate?

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 AutoCloseable cleanup.
  • Catch the narrowest exception type that your code can actually handle.

References

Related YouTube lessons

Next in the Java pathJava Virtual Threads with Safe Shared State →

See the full Java learning path

Keep learning