☕ Java

throws

The throws clause in a method signature declares that the method may throw the listed checked exceptions. It is a contract between the method and its callers: 'I may not complete normally for these reasons, and you must decide how to handle them.' The throws clause is part of the method's API, appearing in generated Javadoc and forcing callers to acknowledge the possibility of failure. This entry covers the precise meaning of throws, when declarations are required vs optional, the rules for overriding methods with throws clauses, the difference between throws and throw, how throws interacts with inheritance, and the design considerations around checked exception declarations.

throws — Declaration and Meaning

The throws clause appears after the method's parameter list and before the method body, declaring one or more exception types the method may throw. It serves as documentation and as a compiler contract for checked exceptions. For checked exceptions, the compiler enforces the declaration: a method that throws a checked exception without catching it must declare it. For unchecked exceptions (RuntimeException subclasses and Errors), the throws declaration is optional — the method can declare them for documentation purposes but is not required to. The throws clause declares what the method can throw, not what it always throws. It is a ceiling, not a guarantee. A method declaring throws IOException may complete normally without throwing anything. Callers must handle the declared exceptions, but they cannot assume the exceptions will always be thrown. A method can declare exceptions it does not directly throw if it calls other methods that throw those exceptions and propagates them up. This is exception propagation through declaration — the method receives a checked exception from a callee, does not catch it, and must therefore declare it. The declaration chain propagates upward through the call stack until some method catches and handles the exception. Multiple exceptions can be declared in a comma-separated list. There is no limit on the number of declared exceptions, but long throws clauses are often a design smell — they may indicate a method that is doing too many unrelated things, or that checked exceptions are being used where unchecked would be more appropriate.
Java
// ── throws declaration syntax ────────────────────────────────────────
// Single checked exception
public void readFile(String path) throws IOException {
    Files.readAllBytes(Path.of(path));
}

// Multiple checked exceptions
public void processOrder(Order order)
        throws OrderValidationException, PaymentException, IOException {
    validateOrder(order);      // throws OrderValidationException
    chargePayment(order);      // throws PaymentException
    generateInvoice(order);    // throws IOException
}

// ── throws for unchecked exceptions — optional, for documentation ─────
public int divide(int a, int b) throws ArithmeticException {
    return a / b;   // throws ArithmeticException if b == 0
    // Declaration optional — ArithmeticException is unchecked
}

// ── throws declares potential, not certainty ──────────────────────────
public void mayOrMayNotThrow(int x) throws IOException {
    if (x < 0) {
        throw new IOException("negative value");
    }
    // May complete normally without throwing
}

// ── Propagation through declaration ──────────────────────────────────
// Low-level method:
public void readBytes(String path) throws IOException {
    Files.readAllBytes(Path.of(path));
}

// Mid-level: does not catch — must declare
public void parseConfig(String path) throws IOException {
    String content = new String(Files.readAllBytes(Path.of(path)));
    parseContent(content);  // no IOException thrown here
}

// High-level: catches — does not need to declare
public Config loadConfig(String path) {
    try {
        parseConfig(path);
    } catch (IOException e) {
        log.warn("Config not found, using defaults: {}", e.getMessage());
        return Config.defaults();
    }
    return Config.loaded();
}

throws and Method Overriding

The throws clause participates in the rules for method overriding. An overriding method in a subclass may only declare exceptions that are the same as or narrower than (subtypes of) the exceptions declared by the overridden method. An override cannot introduce new checked exceptions that the superclass method does not declare, and cannot widen the declared exceptions to a broader type. This rule exists because callers use the superclass type — they expect to handle only what the superclass declares, and a subclass that throws additional checked exceptions would break those callers. An overriding method may declare fewer checked exceptions than the overridden method — it can remove declarations entirely. It may also declare more specific subtypes: if the parent declares throws IOException, the override may declare throws FileNotFoundException (a subtype). Both changes are safe for callers: they can still use the catch blocks they wrote for the superclass API. This constraint applies only to checked exceptions. Unchecked exceptions can always be added or removed from a throws clause without affecting override legality, because unchecked exceptions are never enforced by the compiler.
Java
// ── Overriding rules for throws ──────────────────────────────────────
public class Parent {
    public void method() throws IOException, SQLException { }
}

public class Child extends Parent {

    // LEGAL: same exceptions
    @Override
    public void method() throws IOException, SQLException { }

    // LEGAL: fewer exceptions (removes SQLException)
    @Override
    public void method() throws IOException { }

    // LEGAL: no exceptions (removes all)
    @Override
    public void method() { }

    // LEGAL: more specific subtype (FileNotFoundException ⊂ IOException)
    @Override
    public void method() throws FileNotFoundException { }

    // ILLEGAL: new checked exception not in parent:
    // @Override
    // public void method() throws IOException, ParseException { } // ERROR

    // ILLEGAL: broader exception type:
    // @Override
    // public void method() throws Exception { }  // Exception ⊃ IOException
}

// ── Interface throws clauses ──────────────────────────────────────────
public interface DataLoader {
    void load(String source) throws IOException;
}

// Implementation may declare the same or narrower exceptions:
public class FileLoader implements DataLoader {
    @Override
    public void load(String source) throws FileNotFoundException {
        // FileNotFoundException is a subtype of IOException — legal
    }
}

public class CacheLoader implements DataLoader {
    @Override
    public void load(String source) {
        // Declares nothing — also legal (no checked exceptions)
    }
}

// ── Practical implication: interface shapes the throws clause ─────────
DataLoader loader = new FileLoader();
try {
    loader.load("data.txt");    // Caller sees DataLoader's throws IOException
} catch (IOException e) {      // Must handle IOException (not FileNotFoundException)
    // Correct — caller handles the interface's declared exception
}

throws vs throw — The Distinction

The throw and throws keywords are related but serve entirely different purposes. throw is an executable statement that creates and throws a specific exception instance — it is an action that happens at runtime. throws is a method declaration clause that lists exception types — it is a compile-time declaration that informs callers and the compiler. A method can have throws in its signature without ever executing a throw statement — if the exceptions it declares are only thrown by methods it calls and propagates. A method can execute throw statements for unchecked exceptions without any throws declaration — unchecked exceptions do not require declaration. Understanding this distinction is fundamental to reading and writing Java method signatures correctly. The throws clause is documentation-first for unchecked exceptions. Writing throws NullPointerException in a method signature communicates to callers that null arguments are not accepted and will cause an exception, even though the compiler does not require it. This documentation value is sometimes worth the verbosity, particularly for public APIs where developers need to understand what can go wrong without reading the source code.
Java
// ── throw vs throws — completely different roles ─────────────────────
//
// throw  = RUNTIME ACTION   — executes, throws an exception instance
// throws = COMPILE DECLARATION — part of method signature, informs compiler

// throws in signature (declaration):
public void save(User user) throws ValidationException {

    // throw statement (action):
    if (user == null) throw new NullPointerException("user must not be null");
    if (!user.isValid()) throw new ValidationException("user is invalid");

    repository.save(user);
}

// ── throws without throw ──────────────────────────────────────────────
// This method declares IOException but never directly throws it
// — propagates from readConfig()
public void startup() throws IOException {
    String config = readConfig("app.properties");   // readConfig throws IOException
    applyConfig(config);                             // may not throw
    // No throw statement here — exception propagates from readConfig
}

// ── throw without throws ──────────────────────────────────────────────
// Unchecked exceptions require no throws declaration
public void validate(String email) {
    if (email == null) {
        throw new NullPointerException("email must not be null");  // no declaration needed
    }
    if (!isValidFormat(email)) {
        throw new IllegalArgumentException("Invalid email: " + email);  // no declaration
    }
}

// ── throws for documentation of unchecked exceptions ──────────────────
/**
 * Finds a user by ID.
 *
 * @param id the user ID
 * @return the user
 * @throws UserNotFoundException if no user with the given ID exists (unchecked)
 * @throws IllegalArgumentException if id is null or negative (unchecked)
 */
public User findById(Long id) throws UserNotFoundException {
    // Documenting unchecked exceptions in throws is optional
    // but communicates the contract to callers reading the signature
    Objects.requireNonNull(id, "id must not be null");
    if (id < 0) throw new IllegalArgumentException("id must be positive");
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id));
}

Design Considerations — When to Declare Checked Exceptions

The decision to declare a checked exception in the throws clause shapes the API in lasting ways. Every caller must handle or propagate the exception, and every override must respect the declaration. Choosing checked vs unchecked is therefore a design decision about who should be responsible for handling a failure and whether it is reasonable to expect callers to handle it. The case for checked exceptions: when the failure is a recoverable condition that any reasonable caller should know about and might handle differently. FileNotFoundException is a good example — the caller might want to create the file, use a default value, or ask the user to select a different path. The compiler's enforcement ensures callers cannot accidentally ignore the failure. The case against checked exceptions: when the failure indicates a programming error (wrong argument, invalid state, missed precondition), callers should not catch and recover — they should fix the bug. Also when the failure is infrastructure-level and propagates through many layers — requiring all those layers to declare or catch it creates noisy code with no practical benefit. Most modern frameworks (Spring, Hibernate, Jackson) use unchecked exceptions exclusively for precisely this reason, wrapping lower-level checked exceptions at their boundaries. The practical approach is to use checked exceptions sparingly: only for conditions that the immediate caller (not some caller many layers up the stack) is likely to handle specifically, and where ignoring the condition would be a genuine error of omission rather than a programming mistake.
Java
// ── When checked exceptions make sense ───────────────────────────────

// FileProcessor: caller can meaningfully handle FileNotFoundException
public class FileProcessor {
    public String process(String path) throws FileNotFoundException {
        if (!Files.exists(Path.of(path))) {
            throw new FileNotFoundException("File not found: " + path);
        }
        // ... process file
        return result;
    }
}

// Caller handles it specifically:
try {
    String result = processor.process(path);
} catch (FileNotFoundException e) {
    // Meaningful recovery: ask user to provide the file
    String altPath = promptUser("File not found. Enter correct path:");
    return processor.process(altPath);
}

// ── When unchecked is better ──────────────────────────────────────────

// Service layer: caller cannot meaningfully recover from SQL errors
// Declaring throws SQLException forces every service caller to handle SQL
// Better: wrap at repository boundary, propagate as unchecked
@Repository
public class OrderRepository {
    public Order findById(Long id) {   // NO throws SQLException
        try {
            return jdbcTemplate.queryForObject(...);
        } catch (DataAccessException e) {
            throw new OrderRepositoryException(    // unchecked wrapper
                "Failed to load order " + id, e);
        }
    }
}

// ── The viral declaration anti-pattern ───────────────────────────────
// Checked exception leaking through many layers unnecessarily:
interface UserService {
    User findById(Long id) throws SQLException;  // Why does service declare SQL?
}

interface OrderService {
    Order createOrder(Long userId) throws SQLException;  // Viral propagation
}

interface PaymentService {
    void charge(Long orderId) throws SQLException;  // Everything declares SQL
}

// Better: translate at boundary, use unchecked above it
interface UserService {
    User findById(Long id);   // Clean — SQL is an implementation detail
}

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.