Throwing Exceptions
Fail fast with throw, pick the right built-in exception type, write messages that help, and declare checked exceptions with throws.
throw 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 calladdonList.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
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.
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 2sTurning 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.
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 foundthrow 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.
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