Skip to content
Part IA Michaelmas Term

Exception Guidelines and Assertions

Five Exception Guidelines

1. Never Swallow Exceptions Silently

An empty catch block hides bugs and makes failures untraceable:

try {
    processFile("data.txt");
} catch (IOException e) {
    // silently swallowed — the file might be unreadable and nobody knows
}

At minimum, log the exception. If the program cannot reasonably continue, let the exception propagate or throw a domain-appropriate exception:

try {
    processFile("data.txt");
} catch (IOException e) {
    logger.log(Level.WARNING, "Failed to process file", e);
    throw new UncheckedIOException("Could not process data.txt", e);
}

2. Catch the Most Specific Type You Can Handle

Broad catch (Exception e) masks unrelated bugs and may catch RuntimeException instances that indicate programming errors you should not silence:

try {
    doManyThings();
} catch (Exception e) {  // too broad
    // catches NullPointerException, ClassCastException, etc. — hiding bugs
}

Catch exactly the exception types your code knows how to recover from:

try {
    doManyThings();
} catch (FileNotFoundException e) {
    createDefaultFile();
} catch (IOException e) {
    logger.warning("I/O error: " + e.getMessage());
}

Never catch Throwable — it catches Error instances (OutOfMemoryError, StackOverflowError) that you almost certainly cannot handle correctly.

3. Document Exceptions with @throws

Javadoc @throws tags tell callers what can go wrong without forcing them to read your implementation:

/**
 * Reads and returns all lines from the specified file.
 *
 * @param path the file to read
 * @return the file contents as a list of lines
 * @throws IOException if an I/O error occurs reading the file
 * @throws SecurityException if a security manager denies read access
 */
public List<String> readAllLines(Path path) throws IOException {
    return Files.readAllLines(path);
}

Include both checked and unchecked exceptions that a reasonable caller might want to handle. The Javadoc is the API contract — if your method throws an undocumented exception, callers cannot prepare for it.

4. Avoid Leaking Implementation Exceptions

A repository interface backed by SQL should not force callers to catch SQLException:

interface UserRepository {
    User findById(String id) throws SQLException;  // bad — leaks implementation
}

Callers are now coupled to the fact that the repository uses SQL. If you later switch to a file-based or in-memory store, every caller breaks. Wrap implementation-specific exceptions in domain-appropriate ones:

interface UserRepository {
    User findById(String id) throws DataAccessException;  // good — abstracted
}

User findById(String id) throws DataAccessException {
    try {
        return jdbcTemplate.query(...);
    } catch (SQLException e) {
        throw new DataAccessException("Cannot find user: " + id, e);
    }
}

This is an example of maintaining loose coupling at the exception level — the abstraction boundary should extend to error types.

5. Never Use Exceptions for Ordinary Control Flow

Exceptions are expensive (stack trace capture allocates objects and walks the entire call stack). They also obscure intent — a new developer will assume an exception means something went wrong, not that this is a normal code path:

try {
    return Integer.parseInt(input);
} catch (NumberFormatException e) {
    return 0;  // using an exception to implement "default to 0" — bad
}

Prefer condition-checking for expected conditions:

if (input == null || input.isEmpty()) {
    return 0;
}
try {
    return Integer.parseInt(input);
} catch (NumberFormatException e) {
    // genuinely unexpected — the input should have been validated upstream
    throw new IllegalArgumentException("Invalid number: " + input, e);
}

The rule of thumb: if a condition is expected to occur in normal operation (user typed something non-numeric, file might not exist, network might time out), handle it with control flow; use exceptions for conditions that should not occur in a correct program operating in a correct environment.

Assertions

An assertion is a boolean expression that the programmer believes must always be true at a given point in the program:

assert age >= 0 : "age must be non-negative, got: " + age;

If the assertion fails, the JVM throws AssertionError with the provided message. Assertions are disabled by default at runtime. Enable them with the -ea (enable assertions) JVM flag.

Proper Uses of Assertions

  • Internal invariants: conditions that must hold based on your code’s logic — “this list should never be empty at this point.”
  • Postconditions: after a private method runs, verify that its result satisfies the documented contract.
  • Control-flow invariants: assert false in a code path that should be unreachable — “the default case of an exhaustive switch over an enum.”

Improper Uses of Assertions

  • Validating external/user input: if the user provides an invalid value, that is not a bug in your code — it is a validation failure. Use exceptions (e.g., IllegalArgumentException), which are always enabled.
  • Checking environmental facts: assert dbConnection.isOpen() — the database connection might genuinely be closed due to a network issue, which is not a programming error.
  • Logic that must always run in production: assertions can be disabled. The following is wrong because the side effect (data.processed = true) is lost when assertions are off:
assert data.setProcessed(true);  // WRONG — side effect in assertion

Assertions versus Exceptions

AspectAssertionsExceptions
PurposeCatch programming errors during development and testingHandle runtime conditions in production
Enabled by default?No (use -ea)Yes
Can be recovered?No — throws AssertionError (an Error)Yes — catch block can handle
Use forInternal invariants, postconditionsExternal conditions, user input validation

Assertions are a debugging and documentation aid. They make implicit assumptions explicit in the code. They catch bugs early during development but impose zero runtime cost in production (when disabled). Do not rely on them for any logic that must execute in production.