Throwing Exceptions

Fail fast with throw, pick the right built-in exception type, write messages that help, and declare checked exceptions with throws.

Syntaxthrow new IllegalArgumentException("limit must be positive, got " + limit); Report load(Path file) throws IOException { ... }

throw takes any Throwable instance and stops the current method immediately. Control goes to the nearest matching catch, running any finally blocks on the way. If the exception is checked, the method must either catch it or list it in its throws clause, so callers can see it in the signature.

Order place(String customerId, int quantity) {
    Objects.requireNonNull(customerId, "customerId must not be null");
    if (quantity < 1) {
        throw new IllegalArgumentException("quantity must be at least 1, got " + quantity);
    }
    // the rest only runs with valid input
}

Fail fast with guard clauses

Validate at the top of a method and throw before doing any work. An order that fails validation after half its rows are written is far harder to deal with than one rejected on the first line. Guard clauses also keep the happy path unindented and readable.

Use the JDK types first

  • IllegalArgumentException: an argument is out of range or malformed.
  • NullPointerException: a required argument is null. Objects.requireNonNull(value, message) throws it with your message.
  • IllegalStateException: the arguments are fine but the object is not in a state to do this, such as cancelling an order that has shipped.
  • UnsupportedOperationException: the operation is not supported at all, as when you call add on List.of(...).
  • NoSuchElementException: nothing to return. Optional.orElseThrow() throws it.

These map naturally onto HTTP: argument problems become 400, state problems often 409. Be careful mapping them globally, though: an IllegalArgumentException or NullPointerException thrown by a bug deep in your own code would then reach the client as a 400 instead of a 500. When callers need to tell your failures apart, or handlers need extra data such as a resource id, write a custom exception instead.

Messages that help at 3 a.m.

Put the offending value and the rule in the message: "quantity must be 1-100, got 0" beats "invalid input". Never put passwords, tokens or full card numbers in a message; messages end up in logs and sometimes in API responses.

throw is a statement and an expression arm

A switch expression arm can be a throw, which is the tidy way to reject an unknown value instead of returning a fake default. Likewise orElseThrow(() -> new ...) turns an empty Optional from a lookup into a not-found exception in one line.

A common mistake is creating an exception and forgetting the throw keyword. new IllegalArgumentException("bad"); on its own compiles and does nothing.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Reject a negative page size in a list endpoint before any query runs.
  • Refuse to cancel an order that has already shipped with an IllegalStateException mapped to 409.
  • Turn an empty repository lookup into a not-found exception with orElseThrow.

More examples

Declaring a checked exception with throws

TimeoutException is checked, so the throws clause is required and every caller must decide what to do. Here the caller maps it to a 503, which tells clients to retry later.

Example · java
int stockFor(String sku) throws TimeoutException {
    if (sku.startsWith("SLOW")) {
        throw new TimeoutException("inventory service did not answer in 2s");
    }
    return 12;
}

void main() {
    for (String sku : List.of("BOOK-1", "SLOW-9")) {
        try {
            IO.println(sku + " stock " + stockFor(sku));
        } catch (TimeoutException e) {
            IO.println(sku + " -> 503: " + e.getMessage());
        }
    }
}

// Output:
// BOOK-1 stock 12
// SLOW-9 -> 503: inventory service did not answer in 2s

Turning an empty lookup into a not-found exception

orElseThrow takes a supplier, so the exception is only built when the value is missing. Passing a message with the id makes the log line useful on its own.

Example · java
Map<Long, String> users = Map.of(1L, "ada", 2L, "linus");

String findUser(long id) {
    return Optional.ofNullable(users.get(id))
        .orElseThrow(() -> new NoSuchElementException("user " + id + " not found"));
}

void main() {
    IO.println(findUser(1));
    try {
        findUser(99);
    } catch (NoSuchElementException e) {
        IO.println("404: " + e.getMessage());
    }
}

// Output:
// ada
// 404: user 99 not found

throw as a switch expression arm

Throwing from the default arm is better than returning a made-up status such as 200 or -1, which would let a typo travel silently into a response.

Example · java
int successStatus(String action) {
    return switch (action) {
        case "create" -> 201;
        case "read", "update" -> 200;
        case "delete" -> 204;
        default -> throw new IllegalArgumentException("unknown action: " + action);
    };
}

void main() {
    IO.println(successStatus("create"));
    IO.println(successStatus("delete"));
    try {
        successStatus("archive");
    } catch (IllegalArgumentException e) {
        IO.println(e.getMessage());
    }
}

// Output:
// 201
// 204
// unknown action: archive

Discussion

  • Be the first to comment on this lesson.