Chapter 03 · Reps

Exception Handling — Reps

← Back to Chapter 3

Chapter 3 — Reps

Conditioning, not grading. Exception-handling reps.

Ground rules:

  1. Type every line yourself. No copy-paste of try/catch templates.
  2. Compile and run after every rep. Watch what happens at runtime.
  3. AI stays OFF. Phase 1. Exception handling is one of the rep-heaviest topics in Java; the muscle has to live in your hands.
  4. When the program crashes, read the stack trace top to bottom. That’s how you’ll find every bug for the rest of your career.

Working in OnlineGDB Java 17 or your local JDK. Either path; don’t switch mid-rep.


Reps 1–3: try / catch / finally

Rep 1 — Forced NumberFormatException

Create ParseAge.java:

import java.util.Scanner;

public class ParseAge {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("Enter your age: ");
        String input = in.nextLine();

        try {
            int age = Integer.parseInt(input);
            System.out.println("Next year you will be " + (age + 1) + ".");
        } catch (NumberFormatException e) {
            System.out.println("That wasn't a number. Got: " + input);
        } finally {
            in.close();
            System.out.println("Goodbye.");
        }
    }
}

Run it three times:

  1. Type 25. Confirm normal path.
  2. Type seven. Confirm catch block runs.
  3. Type nothing (just hit enter). Confirm catch block runs (empty string also fails to parse).

In all three cases, “Goodbye.” prints. That’s the finally block doing its job.


Rep 2 — Multi-catch

Modify Rep 1 so that the try block also reads a second integer that gets divided into the first. Catch both NumberFormatException and ArithmeticException in a single multi-catch:

try {
    int a = Integer.parseInt(in.nextLine());
    int b = Integer.parseInt(in.nextLine());
    System.out.println(a + " / " + b + " = " + (a / b));
} catch (NumberFormatException | ArithmeticException e) {
    System.out.println("Bad input: " + e.getClass().getSimpleName() + " — " + e.getMessage());
}

Test with 10 and 2 (normal), ten and 2 (NumberFormatException), 10 and 0 (ArithmeticException). Notice how the catch block tells you which kind it was.


Rep 3 — Try-with-resources

Rewrite Rep 1 to use try-with-resources for the Scanner. The structure becomes:

public class ParseAge {
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in)) {
            System.out.print("Enter your age: ");
            String input = in.nextLine();
            try {
                int age = Integer.parseInt(input);
                System.out.println("Next year you will be " + (age + 1) + ".");
            } catch (NumberFormatException e) {
                System.out.println("That wasn't a number. Got: " + input);
            }
        }
        System.out.println("Goodbye.");
    }
}

The Scanner closes automatically when the outer try block exits — no finally, no explicit close(). This is the modern idiom. Use it for the rest of the course unless you have a specific reason not to.


Reps 4–5: Throwing and Custom Exceptions

Rep 4 — Throw with a useful message

Write a class BankAccount with one method:

public void withdraw(double amount) {
    // your code here
}

The method must:

  • Throw IllegalArgumentException if amount is negative.
  • Throw IllegalStateException (with a useful message including the current balance and the attempted amount) if amount exceeds the balance.
  • Subtract amount from the balance otherwise.

Write a small main that calls withdraw three times — once normally, once with a negative amount, once with too much — wrapped in try/catch blocks that print the exception’s class and message. Confirm the diagnostics are useful.


Rep 5 — Custom Checked Exception with Chaining

Write a custom exception:

public class CatechismLoadException extends Exception {
    public CatechismLoadException(String message) {
        super(message);
    }
    public CatechismLoadException(String message, Throwable cause) {
        super(message, cause);
    }
}

Then write a method:

public static String loadCatechism(String path) throws CatechismLoadException {
    try {
        return java.nio.file.Files.readString(java.nio.file.Path.of(path));
    } catch (java.io.IOException e) {
        throw new CatechismLoadException("Could not load catechism from " + path, e);
    }
}

In main, call loadCatechism("does-not-exist.txt") inside a try/catch. In the catch block, print:

System.out.println("Top-level message: " + e.getMessage());
System.out.println("Cause: " + e.getCause());

Notice that the cause is the original IOException (specifically NoSuchFileException). The chain is preserved.


Reps 6–7: Fail Fast

Rep 6 — Objects.requireNonNull

Write a Person class:

import java.util.Objects;

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = Objects.requireNonNull(name, "name");
        if (age < 0) throw new IllegalArgumentException("age must be non-negative; got " + age);
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
}

Write a main that tries to construct:

  • new Person("Maya", 21) — normal.
  • new Person(null, 21) — should throw NullPointerException("name").
  • new Person("Maya", -1) — should throw IllegalArgumentException with a useful message.

Confirm each diagnostic is precise. Notice how requireNonNull produces a clear message instead of the dreaded NullPointerException with no context.


Rep 7 — Refactor a Brittle Method

Below is a method that crashes ungracefully on bad input. Refactor it to fail fast with clear, specific exceptions.

public static int average(int[] scores, int start, int end) {
    int total = 0;
    for (int i = start; i < end; i++) {
        total += scores[i];
    }
    return total / (end - start);
}

Your refactor should:

  • Throw NullPointerException("scores") if scores is null.
  • Throw IllegalArgumentException if start > end (with a message that includes both values).
  • Throw IndexOutOfBoundsException if start < 0 or end > scores.length.
  • Throw IllegalArgumentException if start == end (empty range — division by zero would otherwise happen).

Write a Javadoc above the method documenting all four @throws. Compile and test each failure path.


Reps 8–9: Graceful Degradation and Logging

Rep 8 — Graceful Input Loop

Write a program that asks the user for a positive integer and keeps asking until they give a valid one. Use try/catch to handle NumberFormatException and a range check to handle non-positive values. Don’t crash. Don’t accept bad input.

import java.util.Scanner;

public class PositiveIntPrompt {
    public static void main(String[] args) {
        try (Scanner in = new Scanner(System.in)) {
            int n = -1;
            while (n <= 0) {
                System.out.print("Enter a positive integer: ");
                String input = in.nextLine();
                try {
                    n = Integer.parseInt(input);
                    if (n <= 0) {
                        System.out.println("Must be positive; try again.");
                    }
                } catch (NumberFormatException e) {
                    System.out.println("Not a number; try again.");
                }
            }
            System.out.println("Thanks. You entered " + n);
        }
    }
}

Test with seven, -3, 0, then finally 42. Confirm the program never crashes and only accepts the valid input.

This is “graceful degradation at the edge” — exactly the pattern Project 3 will ask you to apply to user input.


Rep 9 — Logging with java.util.logging

Write this:

import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileLoader {
    private static final Logger LOG = Logger.getLogger(FileLoader.class.getName());

    public static String load(String path) {
        LOG.info("Attempting to load " + path);
        try {
            String content = Files.readString(Path.of(path));
            LOG.info("Loaded " + content.length() + " characters from " + path);
            return content;
        } catch (IOException e) {
            LOG.log(Level.WARNING, "Failed to load " + path, e);
            return "";
        }
    }

    public static void main(String[] args) {
        String good = load("Hello.java");          // assuming you have one in scratch
        String bad = load("does-not-exist.txt");
        System.out.println("good length: " + good.length());
        System.out.println("bad length:  " + bad.length());
    }
}

Run it. Notice that JUL’s default output format includes the timestamp, the class name, the log level, and (for Level.WARNING with a throwable) the full stack trace. Project 3 Hard will lean on this same pattern.

If you don’t have a Hello.java next to your code, swap in any file you know exists. The bad path should still produce the warning.


Rep 10 — Break a Working Program

Take Rep 6’s Person class. Try the following modifications, one at a time, and observe:

  1. Change Objects.requireNonNull(name, "name") to this.name = name;. Now construct new Person(null, 21). What exception do you see? How does the diagnostic differ from the requireNonNull version?

  2. Remove the if (age < 0) check. Construct new Person("Maya", -5). Notice that the program accepts the bad data and the bug shows up later, when getAge() returns -5 and someone uses it.

  3. Add an empty catch (Exception e) { } around the constructor in the main. Try new Person(null, 21). Notice the program silently continues with a half-constructed object. Never do this in real code. The point of this rep is to feel what silent catch-and-ignore costs.

Restore the correct version. Notice how much more diagnostic value the strict version gives you.


Rep 11 — Read a Stack Trace

Run this on purpose:

public class Crash {
    public static void main(String[] args) {
        String s = null;
        System.out.println(s.length());
    }
}

You’ll get something like:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
    at Crash.main(Crash.java:4)

Notice everything the trace tells you:

  • The exception type (NullPointerException).
  • A precise message (Cannot invoke "String.length()" because "s" is null).
  • The exact file and line (Crash.java:4).
  • The method (Crash.main).

Java’s modern stack traces (Java 14+) tell you which variable was null. Older Java just said NullPointerException with no message — which is one of the reasons Java 17 LTS is a big improvement. Be grateful for it.

Now intentionally produce three more crashes (an ArrayIndexOutOfBoundsException, a NumberFormatException, and a custom one you throw yourself) and read each stack trace top to bottom. Read every one. The trace is the diagnostic.


Done? One Last Thing.

From scratch, no looking — write a program that:

  1. Defines a custom checked exception BadVerseException.
  2. Has a method parseVerse(String input) that splits a Bible verse reference like "Romans 8:28" into a book name, chapter, and verse number — throwing BadVerseException (with a useful message and the original NumberFormatException chained as the cause) if the input is malformed.
  3. Has a main that tries three inputs: a valid one, a malformed one, and null (which should fail with NullPointerException from requireNonNull).
  4. Catches every exception and prints both the message and the cause.

If you can write that cold, you have the move.


Up next: Project 3 — Project 3: The Robust Refactor.