⚡ Section 7 · Question #21

What does the throws keyword do

The throws keyword is declared in a method or constructor signature to specify which checked exceptions the method may potentially propagate to its callers.


🟢 Junior Level

Short Interview Answer (30 seconds)

The throws keyword is declared in a method or constructor signature to specify which checked exceptions the method may potentially propagate to its callers.

It forms an explicit component of the method’s public API contract. The presence of throws enforces the compiler’s “Catch or Specify” requirement: any calling code must either:

  1. Handle the exception locally using a try-catch block.
  2. Re-declare the exception (or its supertype) in its own method header via throws.

Key Distinctions: throw vs. throws

Feature throw throws
Location Inside method bodies or code blocks In method or constructor signature headers
Role Imperative Action: instantiates and triggers exception dispatch Declarative Contract: advertises potential escaping exception types
Syntax throw new IllegalArgumentException("Invalid"); public void process() throws IOException;
Operand A single instantiated object (Throwable instance) A comma-delimited list of exception class types

Canonical Java 21 Example

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class ConfigurationService {

    // Method declares that it may fail with a checked IOException
    public List<String> readLines(String filePath) throws IOException {
        return Files.readAllLines(Path.of(filePath));
    }

    // Approach 1: Caller handles the exception locally
    public void loadLocal(String path) {
        try {
            List<String> lines = readLines(path);
            System.out.println("Loaded lines: " + lines.size());
        } catch (IOException e) {
            System.err.println("Failed to read configuration: " + e.getMessage());
        }
    }

    // Approach 2: Caller delegates responsibility further up the call stack
    public void loadAndPropagate(String path) throws IOException {
        List<String> lines = readLines(path);
        // Process configuration lines...
    }
}

🟡 Middle Level

Method Overriding & The Liskov Substitution Principle (LSP)

When a subclass overrides a superclass method or implements an interface method, polymorphism in Java strictly restricts the throws clause in compliance with LSP:

┌────────────────────────────────────────────────────────────────────────────────────────┐
│                        METHOD OVERRIDING THROWS COMPATIBILITY                          │
├────────────────────────────────────────────────────────────────┬───────────────────────┤
│ Overriding Method Signature                                    │ Compiler Verdict      │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ Same checked exceptions as parent                              │ ✅ Allowed            │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ Narrower / Subclass checked exceptions (Covariant Exceptions)  │ ✅ Allowed            │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ Omits checked exceptions entirely                              │ ✅ Allowed            │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ Any unchecked exceptions (RuntimeException / Error)            │ ✅ Allowed            │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ New checked exceptions not declared in parent                  │ ❌ COMPILATION ERROR  │
├────────────────────────────────────────────────────────────────┼───────────────────────┤
│ Broader checked supertype (e.g., Exception instead of IOEx)    │ ❌ COMPILATION ERROR  │
└────────────────────────────────────────────────────────────────┴───────────────────────┘
public interface DataRepository {
    void persist() throws IOException;
}

public class FileRepository implements DataRepository {
    // 1. Covariant narrowing: FileNotFoundException is a subclass of IOException
    @Override
    public void persist() throws java.io.FileNotFoundException { }
}

public class MemoryRepository implements DataRepository {
    // 2. Omitting: Implementation is guaranteed never to fail with checked errors
    @Override
    public void persist() { }
}

public class DatabaseRepository implements DataRepository {
    // 3. ❌ ILLEGAL: Adding an unrelated checked exception breaks polymorphism!
    // Compilation Error: overridden method does not throw java.sql.SQLException
    // @Override
    // public void persist() throws SQLException { }
}

Architectural Rationale: A caller holding a reference to DataRepository expects only IOException. If DatabaseRepository were allowed to throw SQLException, the caller’s catch (IOException e) block would miss it, causing an unhandled runtime failure and breaking type safety.


Unchecked Exceptions in throws Clause

Java syntax permits declaring unchecked exceptions (RuntimeException and Error) in the throws clause:

// Technically legal syntax, but considered an architectural code smell:
public void calculateRate(int rate) throws IllegalArgumentException, NullPointerException {
    // ...
}

Engineering Rule: Avoid declaring unchecked exceptions in throws. The compiler does not verify them, they add visual clutter, and they mislead developers into believing handling is mandatory. Instead, document unchecked exceptions via Javadoc tags:

/**
 * @throws IllegalArgumentException if rate is negative
 */
public void calculateRate(int rate) { ... }

JVM Bytecode Internals: The Exceptions Attribute

The JVM runtime does not enforce throws contracts during execution. When javac compiles a method declaring throws, it records the exception class symbolic references in a metadata attribute table named Exceptions:

// Disassembled via javap -v ConfigurationService.class:
public java.util.List<java.lang.String> readLines(java.lang.String) throws java.io.IOException;
  descriptor: (Ljava/lang/String;)Ljava/util/List;
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=2, args_size=2
    ...
  Exceptions:
    throws java.io.IOException

This attribute exists solely for static analysis by the javac compiler. The bytecode instruction athrow operates identically whether an exception was declared in Exceptions or not. Reflection, bytecode instrumentation, or generic type erasure (Sneaky Throws) can emit checked exceptions from methods that declare no throws clause without JVM runtime errors.


🔴 Senior Level

Constructor Inheritance Trap: Superclass Exceptions

Constructors follow opposite rules from methods regarding throws:

public class BaseResource {
    public BaseResource() throws IOException {
        // Allocates OS file descriptor
    }
}

public class SecureResource extends BaseResource {
    // Subclass constructor CANNOT remove or narrow the parent exception!
    // It MUST declare IOException (or a wider superclass like Exception)!
    public SecureResource() throws IOException, GeneralSecurityException {
        super(); // Mandatory first statement; cannot be enclosed in try-catch!
    }
}

Unlike regular methods, a subclass constructor cannot eliminate checked exceptions declared by its parent constructor. Because Java requires super(...) to be the absolute first statement of a constructor body, wrapping super() in a try-catch block is syntactically prohibited.


Generic Exception Signatures (<E extends Throwable>)

Java Generics enable dynamic, precise exception transparency in functional interfaces and utility decorators:

@FunctionalInterface
public interface ThrowingSupplier<T, E extends Throwable> {
    T get() throws E;
}

public final class RetryExecutor {

    // E is dynamically inferred based on the lambda's exact thrown checked exception
    public static <T, E extends Throwable> T executeWithRetry(ThrowingSupplier<T, E> action) throws E {
        try {
            return action.get();
        } catch (Throwable t) {
            // Under generic type erasure, javac allows propagating E without re-wrapping
            @SuppressWarnings("unchecked")
            E typedException = (E) t;
            throw typedException;
        }
    }

    public static void main(String[] args) {
        try {
            // Compiler precisely infers E as java.sql.SQLException
            String data = executeWithRetry(() -> queryDatabase());
        } catch (java.sql.SQLException e) {
            System.err.println("Database query failed: " + e.getErrorCode());
        }
    }

    private static String queryDatabase() throws java.sql.SQLException { return "ok"; }
}

Binary Compatibility vs. Source Compatibility in Library Evolution

Altering a public method’s throws clause has severe backward-compatibility implications:

  1. Adding a Checked Exception: Breaks Source Compatibility. Downstream consumer applications will immediately fail compilation upon upgrading the library version.
  2. Removing a Checked Exception: Maintains Source and Binary Compatibility. Compiled client .class files continue linking without errors at runtime.
  3. The “Throws Umbrella” Anti-Pattern: Declaring throws Exception or throws Throwable on public interfaces destroys domain intent, forces callers into writing generic catch (Exception e) handlers, and masks critical runtime bugs.

4 Tricky Questions

1. What happens if a class implements two interfaces that declare the same method, but with different throws clauses: InterfaceA { void run() throws IOException; } and InterfaceB { void run() throws SQLException; }?

Answer: The implementing class must satisfy the intersection of both contracts. Because a client may invoke the method via either InterfaceA or InterfaceB, the implementation cannot throw IOException (which breaks InterfaceB) nor SQLException (which breaks InterfaceA).

Therefore, the implementing method cannot declare any checked exceptions (or can only declare checked exceptions that are common subclasses of both IOException and SQLException, which in this case do not exist). The implementing class must declare public void run() { ... } with no throws clause.


2. Can you declare throws on a static or private method? Does polymorphism apply to them?

Answer: Yes, throws can be declared on static and private methods to enforce the “Catch or Specify” rule on their callers.

However, polymorphism and LSP rules do not apply to them:

  • private methods are not inherited or overridden; a child class method with the same name is an unrelated, independent method and may declare completely different exceptions.
  • static methods are hidden (shadowed), not overridden, so their throws clauses are resolved statically based on the compile-time variable reference.

3. Why can an overriding method eliminate throws IOException, but a subclass constructor cannot eliminate throws IOException from its superclass constructor?

Answer: An overriding method can eliminate the exception because a caller expecting an IOException is completely safe if no exception is ever thrown (LSP compliance).

A subclass constructor, however, must execute super(...) to initialize the superclass state before its own constructor body executes. Since Java language rules forbid wrapping super() in a try-catch block, any exception thrown by super() will inevitably propagate out of the subclass constructor. Therefore, the subclass constructor must declare that exception in its own throws clause.


4. What happens at the JVM bytecode level if a method throws a checked exception that was NOT listed in its throws attribute?

Answer: The JVM executes normally! At the bytecode level, the JVM executes the athrow instruction, which pops an object reference from the operand stack and initiates exception handling by traversing the method’s Exception_table and caller stack frames.

The JVM runtime never checks whether the exception class matches the Exceptions attribute in the classfile. The checked exception requirement is strictly a static compile-time constraint enforced by javac.


🎯 Interview Cheat Sheet

Core Concepts Matrix

┌───────────────────────────┬──────────────────────────────────────────┬──────────────────────────────────────────┐
│ Principle                 │ Valid / Recommended                      │ Invalid / Code Smell                     │
├───────────────────────────┼──────────────────────────────────────────┼──────────────────────────────────────────┤
│ Method Overriding         │ Narrowing (FileNotFoundException)        │ Broadening (throws Exception)            │
│ Overriding Removal        │ Completely removing throws               │ Adding new checked exception (SQLEx)     │
│ Subclass Constructor      │ Broadening or declaring parent exception │ Attempting to suppress parent throws     │
│ Unchecked Exceptions      │ Document via @throws Javadoc tag         │ Declaring throws RuntimeException        │
│ Generalization            │ Specific domain exceptions               │ "Throws Umbrella" (throws Exception)     │
│ Generic Exceptions        │ <E extends Throwable> throws E           │ Raw untyped generic throws               │
└───────────────────────────┴──────────────────────────────────────────┴──────────────────────────────────────────┘

Key Takeaways

  1. throws advertises potential checked exceptions in a method or constructor header, enforcing compile-time handling.
  2. In overridden methods, the throws clause can narrow (covariant) or completely eliminate checked exceptions, but can never widen or add new checked exceptions.
  3. Subclass constructors cannot remove checked exceptions declared by their superclass constructor because super() cannot be wrapped in a try-catch.
  4. Declaring unchecked exceptions (RuntimeException) in throws is technically permitted but discouraged.
  5. The throws clause is stored in the .class metadata as the Exceptions attribute and is enforced purely by javac, not the JVM runtime.

Red Flags to Avoid

  • ❌ Confusing throw (imperative statement) with throws (declarative method header clause).
  • ❌ Claiming that subclasses can declare new checked exceptions when overriding interface or parent methods.
  • ❌ Believing that the JVM runtime verifies throws declarations when executing the athrow instruction.
  • ❌ Putting throws Exception on public service interfaces as a shortcut to avoid writing specific exception classes.