CSCD210

Throwing exceptions yourself

Skillcscd210-throwing-exceptions-yourselfTextbookBJP Ch 6

throw new IllegalArgumentException(...) for invalid input

Student can produce a precondition check at the top of a method body that throws IllegalArgumentException with an informative message, identify that IllegalArgumentException is unchecked (no throws clause required), and explain why fail-fast precondition checking is preferable to in-line conditional logic.

When a method receives an argument that violates its precondition, the right response is to fail fast: reject the call before it produces wrong output. The Java idiom is throw new IllegalArgumentException(message) at the top of the method body.

public static int countLines(final String filename) throws FileNotFoundException {
    if (filename == null) {
        throw new IllegalArgumentException("filename must not be null");
    }
    if (filename.isEmpty()) {
        throw new IllegalArgumentException("filename must not be empty: \"" + filename + "\"");
    }

    //  ... actual work below this point trusts that filename is well-formed ...
    final Scanner sc = new Scanner(new File(filename));
    // ...
}

Five properties of the precondition-check idiom:

1. IllegalArgumentException is unchecked: extends RuntimeException. No throws clause needed on the method header, by the checked against unchecked rule. 2. **The check happens at the start of the method.** Code that depends on the argument's validity runs only after the checks have passed. The body never has to "if argument is OK, do the real work" branches. 3. The message names the parameter, the constraint, AND the bad value. The rule that the message must include the bad value covers the message-construction rules in detail. 4. Fail fast. The exception fires before any state is changed, any file is opened, any partial work is done. The caller can correct the call site and retry without cleanup. 5. The standard library is the model. Look at Arrays.copyOf(int[] original, int newLength): it throws IllegalArgumentException if newLength < 0. String.charAt(int) throws StringIndexOutOfBoundsException (a subclass of IllegalArgumentException's sibling) for negative indices. Every JDK method does precondition checking; your code should too.

The standard exception roster for preconditions

For most "argument is invalid" cases, IllegalArgumentException is the right choice. The JDK reserves a few subclasses and siblings for specific failure modes:

| Condition | Exception | |-----------|-----------| | Argument is wrong in general | IllegalArgumentException | | Argument is null when non-null required | NullPointerException or IllegalArgumentException (Bloch Item 72 prefers NPE; CSCD 210 prefers IAE for clarity) | | Index argument is out of range for a sequence | IndexOutOfBoundsException | | Method called when object is in wrong state | IllegalStateException |

CSCD 210 uses IllegalArgumentException for all four conditions to keep the message-format consistent. CSCD 211 picks the more-specific exception per Bloch's recommendation.

The JDK one-liner for null checks: Objects.requireNonNull

The industry-canonical null-check idiom uses java.util.Objects:

public static int countLines(final String filename) throws FileNotFoundException {
    Objects.requireNonNull(filename, "filename must not be null");
    // ... rest of the method ...
}

Objects.requireNonNull(T, String) throws NullPointerException with the supplied message when the first argument is null, and returns the argument otherwise (so it composes inside expressions: this.name = Objects.requireNonNull(name, "name");). This is the form every modern Java code-base uses for non-null preconditions, and every code reviewer recognizes.

CSCD 210 weeks 6–7 stick with throw new IllegalArgumentException(...) for consistency with the lab style. CSCD 211 introduces Objects.requireNonNull once the import-and-API-discovery muscle is built. Both forms produce a clear failure at the call site; the differences are stylistic (NPE vs IAE) and ergonomic (one-liner vs three-line if).

Why not return a sentinel?

The "return -1 on bad input" pattern, the not-found sentinel convention, works for recoverable absence (no match found). It does not work for precondition violation (caller passed garbage):

  • Sentinel return forces every caller to check the return value; one missed check propagates corrupt data.
  • Sentinel return loses the message that explains why the input was rejected.
  • Sentinel return interleaves "succeed" and "fail" paths into the caller; exception throws keeps them cleanly separated.

The CSCD 210 boundary: sentinel returns for absence (linear search, hash table miss); exceptions for violation (null argument, out-of-range parameter).

In other languages

  • Python: raise ValueError(f"argument must be non-negative: {n}"): direct analog.
  • C++: throw std::invalid_argument("..."): same.
  • Rust: panic!("...") for unrecoverable; Result<T, ErrType> for recoverable. The boundary is more explicit.
  • Go: Go's panic("...") is the analog; return err is the alternative for recoverable.

Exception messages must include the bad value

Student can write an IllegalArgumentException whose message includes (a) the parameter name, (b) the constraint that was violated, and (c) the actual offending value, separated by a colon.

When code rejects an input by throwing IllegalArgumentException, the message string is the only thing the caller sees. A good message names what failed and the specific value that caused the failure: never just "bad input" or "invalid argument."

// Weak: gives the caller no information
throw new IllegalArgumentException("bad input");

// Better: names the parameter
throw new IllegalArgumentException("filename invalid");

// Right: names the parameter AND includes the offending value
throw new IllegalArgumentException("filename must not be null or empty: " + filename);
throw new IllegalArgumentException("count must not be negative: " + count);
throw new IllegalArgumentException("file does not exist: " + filename);

The improvement matters because exception messages are debugging tools. The caller (including the autograder, the next developer, and the student themselves at 2 AM) needs to know which value tripped the check. A message like "bad input" requires reading the source code to find the check; a message like "file does not exist: data/imnts.txt" points directly at the typo.

Three properties of a good message

1. Subject: what kind of value was wrong (filename, count, index). 2. Constraint: what the value violated (must not be null or empty, must not be negative, out of bounds). 3. Actual value: the value the caller passed (null, -3, "data/imnts.txt", 7 when the length was 5).

The standard format combines all three with a colon: "<subject> <constraint>: <actual>".

Why null shows up cleanly

When filename == null, the concatenation "filename must not be null or empty: " + filename produces the string "filename must not be null or empty: null". That is exactly what you want: the message tells the caller the value was null (rather than just being silent about it). Java's + operator on String + null produces "null" via the String.valueOf path.

Three things the message should not do

  • Do not include private/internal state the caller has no use for (a hash, a counter, an internal field). Keep the message focused on the parameter that failed.
  • Do not try to "fix" the value in the message ("filename should probably be 'data/ints.txt'"). The message reports facts; suggestions belong in documentation.
  • Do not use a different format every time. Use the "<subject> <constraint>: <actual>" shape everywhere so callers (and autograders) can match against it.

In other languages

  • Python: raise ValueError(f"filename must not be empty: {filename!r}"): same pattern, f-string syntax.
  • C++: throw std::invalid_argument("filename empty: " + filename);: string concatenation.
  • Rust: panic!("filename invalid: {:?}", filename): same idea, different macro.

Every mainstream language has the same convention: name the parameter, state the constraint, show the value.