throw
The throw statement explicitly throws an exception object, immediately terminating normal execution and initiating exception propagation. throw is used to signal that a method cannot fulfil its contract because a precondition is violated, an invalid state has been detected, or an operation has failed. Understanding when to throw, what to throw, how to create informative exception objects, and how throwing interacts with the call stack is the foundation of using exceptions as a design tool rather than just an error mechanism. This entry covers the throw statement mechanics, throwing at the right abstraction level, creating informative exception messages, exception chaining, and the principles for deciding when to throw versus returning a special value.
The throw Statement — Mechanics
// ── throw statement basics ───────────────────────────────────────────
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException(
"Withdrawal amount must be positive: " + amount);
}
if (amount > balance) {
throw new InsufficientFundsException(amount, balance);
}
balance -= amount;
}
// ── Execution stops at throw ──────────────────────────────────────────
public void process(String input) {
System.out.println("Before validation");
if (input == null) {
throw new NullPointerException("input must not be null");
}
System.out.println("After validation — only runs if no throw");
// Further processing...
}
// ── Stack trace recorded at construction, not at throw ────────────────
// Constructing here records THIS location in the stack trace:
RuntimeException prebuilt = new RuntimeException("pre-built");
// ...later...
throw prebuilt; // stack trace points to construction site, not here
// Better: construct at the throw site:
throw new RuntimeException("constructed at throw site");
// Stack trace points to this line — more useful for debugging
// ── Checked vs unchecked throw requirements ────────────────────────────
public void readFile_unchecked() {
throw new RuntimeException("unchecked — no declaration needed");
}
public void readFile_checked() throws IOException { // must declare
throw new IOException("checked — must declare in throws clause");
}
// ── Rethrowing ────────────────────────────────────────────────────────
public void process_rethrow() throws IOException {
try {
riskyOperation();
} catch (IOException e) {
log.error("Failed to process", e);
throw e; // rethrow the SAME exception object — stack trace preserved
}
}Throwing at the Right Abstraction Level
// ── Wrong level: exposes implementation details ───────────────────────
public class UserRepository {
public User findById(Long id) throws SQLException { // exposes SQL
// Caller is forced to handle SQL concerns at business layer
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?", id, USER_MAPPER);
}
}
// ── Right level: domain exception at domain abstraction ───────────────
public class UserRepository {
public User findById(Long id) {
try {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?", id, USER_MAPPER);
} catch (EmptyResultDataAccessException e) {
throw new UserNotFoundException(id); // domain exception
} catch (DataAccessException e) {
throw new UserRepositoryException( // domain exception
"Failed to find user " + id, e); // with cause
}
}
}
// ── Precondition checking patterns ────────────────────────────────────
public class BankAccount {
private final String id;
private double balance;
public BankAccount(String id, double initialBalance) {
this.id = Objects.requireNonNull(id, "id must not be null");
this.balance = Objects.requireNonNull(initialBalance, "balance must not be null");
if (initialBalance < 0) {
throw new IllegalArgumentException(
"Initial balance cannot be negative: " + initialBalance);
}
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException(
"Deposit amount must be positive, got: " + amount);
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException(
"Withdrawal amount must be positive, got: " + amount);
if (amount > balance) throw new IllegalStateException(
String.format("Insufficient funds: requested %.2f, available %.2f",
amount, balance));
balance -= amount;
}
}
// ── Objects.requireNonNull idiom ──────────────────────────────────────
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryService inventory;
private final PaymentService payments;
public OrderService(OrderRepository orderRepo,
InventoryService inventory,
PaymentService payments) {
this.orderRepo = Objects.requireNonNull(orderRepo, "orderRepo");
this.inventory = Objects.requireNonNull(inventory, "inventory");
this.payments = Objects.requireNonNull(payments, "payments");
}
}Exception Chaining and Informative Messages
// ── Exception chaining — preserving causal history ───────────────────
// Without chaining — root cause lost:
catch (SQLException e) {
throw new DataException("Database error"); // cause not preserved
}
// With chaining — full history preserved:
catch (SQLException e) {
throw new DataException("Failed to save order " + order.getId(), e);
}
// Stack trace shows:
// DataException: Failed to save order 42
// at OrderRepository.save(OrderRepository.java:58)
// ...
// Caused by: java.sql.SQLException: Connection refused
// at com.mysql.jdbc.Driver.connect(Driver.java:...)
// ...
// ── getCause() traversal ──────────────────────────────────────────────
Throwable root = exception;
while (root.getCause() != null) root = root.getCause();
System.out.println("Root cause: " + root.getMessage());
// ── Informative exception messages ───────────────────────────────────
// POOR — no actionable information
throw new IllegalArgumentException("Invalid input");
throw new IllegalStateException("Error");
throw new NullPointerException("null");
// GOOD — specific, actionable context
throw new IllegalArgumentException(
"Age must be between 0 and 150 inclusive, got: " + age);
throw new IllegalStateException(
"Cannot ship order " + orderId +
" in status " + currentStatus +
" — only CONFIRMED orders can be shipped");
throw new NullPointerException(
"orderId must not be null " +
"(passed to OrderService.findById)");
// ── when to throw vs when to return special value ─────────────────────
// Return Optional — "no result" is a valid outcome, not an error
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email); // may legitimately be empty
}
// Throw — "not found" when ID was required to exist is an error
public User getById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Return boolean — validation result is a normal outcome
public boolean isValidEmail(String email) {
return EMAIL_PATTERN.matcher(email).matches();
}
// Throw — invalid argument violates the calling code's contract
public void sendEmail(String email) {
if (!isValidEmail(email)) {
throw new IllegalArgumentException(
"Invalid email address: " + email);
}
// send...
}