☕ Java

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

The throw statement takes a single expression that must evaluate to an instance of Throwable. The expression is evaluated, the resulting exception object is thrown, and execution of the current method immediately stops at the throw statement. No further statements in the method execute. The JVM unwinds the call stack, looking for a matching catch block, and if none is found, the exception propagates to the thread's top-level handler. The thrown object carries all the context information available at the moment of throwing: the message string, the cause (for exception chaining), and the stack trace which is recorded automatically when the exception object is constructed (not when it is thrown). This means an exception object constructed early and thrown later will have a stack trace pointing to the constructor call, not the throw statement — a subtle point that matters when exceptions are pre-constructed and stored before throwing. A throw statement that throws an unchecked exception (RuntimeException or its subclasses, or Error) requires no method declaration. A throw statement that throws a checked exception requires the method to either catch it or declare it in its throws clause. This enforcement is the checked exception mechanism, and the compiler enforces it at every potential throw site.
Java
// ── 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

One of the most important principles for throw usage is throwing exceptions at the right level of abstraction. A method should throw exceptions that describe failures in terms of its own abstraction, not in terms of its implementation. A UserRepository.findById() should throw UserNotFoundException, not SQLException. A PaymentService.charge() should throw PaymentFailedException, not HttpClientException. The caller works at the domain level and should receive domain-level exceptions. This principle is called exception translation. When a lower-level method fails with an infrastructure exception (database error, network error, I/O error), the catching method translates it into a domain exception with domain-relevant context before rethrowing. The original exception is preserved as the cause, so no debugging information is lost. The caller sees a meaningful domain exception; a developer diagnosing a failure can see the full chain including the root cause. Precondition checking with throw is the mechanism for enforcing method contracts. The Objects.requireNonNull() utility method is idiomatic for null checks — it throws NullPointerException with a meaningful message if the argument is null and returns the argument otherwise, allowing assignment in a single line. For other preconditions, throwing IllegalArgumentException with a descriptive message is the convention for invalid argument values, and IllegalStateException for invalid object state.
Java
// ── 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 preserves the full causal history of a failure. When catching one exception and throwing another, always pass the original as the cause argument. The cause is stored in the exception object, appears in the stack trace with "Caused by:" sections, and is accessible via getCause(). A chain of "Caused by:" sections lets a developer trace a failure from the user-facing error message back to the root technical cause without losing any information along the way. Exception messages should be informative enough to diagnose the problem without looking at the code. A good exception message includes: what was being attempted, what the invalid value or state was, and what was expected. "Invalid age" is a poor message; "Age must be between 0 and 150 inclusive, got: -5 for user alice@example.com" is excellent. The extra information costs almost nothing at throw time (the exception is already being constructed) but can save hours of debugging. When to throw vs when to return a special value (null, Optional.empty(), -1) is a design question. The guideline: throw when the operation cannot produce a valid result and the caller cannot reasonably continue without handling the failure explicitly. Return a special value when "no result" is a normal, expected outcome that the caller will handle in normal flow. A search that finds nothing is not an error — return Optional.empty(). A configuration value that is required but missing is an error — throw.
Java
// ── 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...
}

Related Topics in Exception Handling

Exception Basics
An exception is an event that disrupts the normal flow of a program during execution. Java's exception system is a structured mechanism for signalling, propagating, and handling error conditions — a significant improvement over the older approach of returning special error codes that callers could silently ignore. Understanding exceptions means understanding the class hierarchy that categorises them, the distinction between checked and unchecked exceptions, what the JVM does when an exception is thrown, how the call stack is unwound, and what information an exception object carries. This entry covers the full exception hierarchy, the checked vs unchecked distinction and the reasoning behind it, exception propagation through the call stack, and the information model of exception objects.
try-catch
The try-catch statement is Java's primary mechanism for handling exceptions. Code that might throw an exception is placed in the try block; one or more catch blocks follow, each specifying the exception type to handle and providing the handling logic. When an exception is thrown inside the try block, execution of the try block immediately stops and the JVM searches the catch blocks in order for the first one whose type matches the thrown exception. Understanding the control flow precisely — what runs, what stops, and in what order — is essential for writing correct exception handling code. This entry covers try-catch control flow, exception type matching, catching by supertype, variable scope, and the key principles for writing good catch blocks.
Multiple catch
A single try block can be followed by multiple catch blocks, each handling a different exception type. Multiple catch blocks allow code to respond differently to different kinds of failures — recovering from an expected missing file, but reporting and rethrowing an unexpected database error. Java 7 introduced multi-catch syntax, allowing a single catch block to handle multiple unrelated exception types, eliminating the duplicate code that arises when different exceptions require the same handling. This entry covers the ordering rules, multi-catch syntax, the type of the caught variable in multi-catch, and the design patterns for structuring multiple exception handlers.
finally
The finally block contains code that must execute regardless of whether the try block completed normally, threw an exception that was caught, or threw an exception that was not caught. It is the mechanism for guaranteed cleanup — closing files, releasing locks, returning connections to a pool, rolling back transactions. The finally block runs in all scenarios except two: if the JVM exits (System.exit()) or if the thread is killed by an Error like StackOverflowError. Understanding when exactly finally runs, the interaction between finally and return statements, exception suppression when finally itself throws, and the modern alternative of try-with-resources is essential for writing correct resource management code in Java.