How does the exception variable's type in a multi-catch interact with the methods you can call and with precise rethrow analysis?
answer
- variable type = least upper bound (lub) of alternatives
- only lub's methods callable; instanceof+cast to narrow
- precise rethrow = Java 7 tracks actual thrown types
- requires the catch param effectively final
- keeps throws clause narrow despite broad catch
basics
~20 sIn multi-catch the variable is typed as the common parent of the listed types, so you can only call methods that parent declares. Java's 'precise rethrow' still tracks the real types, so rethrowing the variable can satisfy narrower throws clauses.
solid answer
~50 sIn a multi-catch, the exception variable's static type is the **least common supertype (lub)** of the listed alternatives, so the methods available on it are only those declared by that supertype — to use a subtype-specific method you must instanceof-check and cast. Separately, Java 7 introduced **precise rethrow**: when you catch (Exception e) (or a multi-catch) and throw e; without reassigning, the compiler doesn't treat the rethrow as throwing the broad declared type. Instead it analyzes which checked exceptions could *actually* reach that catch and lets the enclosing method's throws clause list only those narrower types. Combined with the implicitly-final multi-catch variable, this lets you catch several specific exceptions, do shared work, and rethrow while keeping a precise, narrow throws signature — improving exception transparency for callers. The two features were designed together in Project Coin to make broad catch-and-rethrow both concise and type-precise.
code
java · 9 linesString load() throws ParseException, IOException { // precise throws, not 'throws Exception'
try {
return parse(read());
} catch (ParseException | IOException e) { // e has type Exception (the lub)
metrics.increment("load.failure");
if (e instanceof IOException io) retryLater(io); // narrow to call subtype method
throw e; // precise rethrow keeps throws narrow
}
}go deeper
Aware the variable only exposes shared (parent) methods and that rethrow exists, without the analysis details.
Can explain you must instanceof/cast for subtype methods, and has heard of precise rethrow keeping throws narrow.
Explains the lub typing and precise rethrow together, including the effectively-final precondition and the catch-share-rethrow idiom.
Reasons about exception transparency, JLS definite-assignment soundness, intersection-type lubs, and API-contract implications of preserving narrow throws clauses across large codebases.
## Two distinct ideas, often confused This question joins **(1)** what type the multi-catch variable has (and thus which methods you can call) with **(2)** the Java 7 *precise rethrow* analysis. Both shipped together but they answer different questions. ## (1) The variable's type — the least upper bound When you write `catch (A | B | C e)`, the compiler assigns `e` the **least common supertype** of A, B, and C — formally the **lub** (least upper bound) in the class hierarchy. If `IOException` and `SQLException` are the alternatives, their nearest shared ancestor is `Exception`, so `e` is of static type `Exception`. Consequence: **only members declared on that lub are callable.** ```java catch (IOException | SQLException e) { e.getMessage(); // OK — declared on Throwable/Exception e.getErrorCode(); // COMPILE ERROR — that's SQLException-only } ``` To reach a subtype-specific method you must narrow at runtime: ```java catch (IOException | SQLException e) { if (e instanceof SQLException sql) { // pattern instanceof int code = sql.getErrorCode(); } } ``` (If the lub is an *intersection* of types — possible with interfaces like `Serializable` — you can call members of all the intersected types; but the common case is a single class.) ## (2) Precise rethrow Before Java 7, this method had to declare `throws Exception`, losing precision for callers: ```java void m() throws Exception { // pre-7: forced to be broad try { dangerous(); } // dangerous() throws IOException, SQLException catch (Exception e) { cleanup(); throw e; // pre-7: compiler thinks this throws Exception } } ``` From Java 7, the compiler performs **precise rethrow analysis**: if a catch parameter is **not reassigned** in the block, then a `throw e;` is treated as throwing **only the exception types that could actually reach that catch** — derived from the `try` body — not the broad declared catch type. So: ```java void m() throws IOException, SQLException { // now precise! try { dangerous(); } catch (Exception e) { cleanup(); throw e; // analyzed as IOException|SQLException, not Exception } } ``` The key condition is **the parameter must be effectively final** (not reassigned). This is exactly why multi-catch parameters are *implicitly final* — it guarantees the analysis is sound, because the variable provably still holds the originally-caught object. ## How the two combine Together they enable the idiomatic Java 7+ pattern: **catch broadly or via multi-catch, do shared handling/cleanup, and rethrow with a precise narrow `throws`**: ```java String load() throws ParseException, IOException { try { return parse(read()); } catch (ParseException | IOException e) { metrics.increment("load.failure"); throw e; // precise: throws clause stays ParseException, IOException } } ``` Callers still see the exact checked exceptions; you didn't have to widen the signature to `throws Exception` just to share a handler. This is what the JLS/Project Coin called improving **exception transparency**. ## Edge cases & gotchas - **Reassigning the variable defeats precise rethrow.** If you do `e = new Exception();` (only possible in single-catch, since multi-catch forbids it), the rethrow falls back to the declared/assigned type. This is another reason multi-catch forces finality. - **Only checked exceptions matter for `throws`.** Unchecked (`RuntimeException`) types don't need declaring either way. - **Pattern matching** (`instanceof SQLException sql`) is the clean modern way to recover subtype methods inside a broadened catch. ## Summary The multi-catch variable is typed as the alternatives' least upper bound (limiting callable methods to that supertype), while precise rethrow — enabled because the parameter is effectively/implicitly final — lets a `throw e;` propagate the *actual* narrow exception set. The design lets you consolidate handling without sacrificing the precision of your method's `throws` contract.
- What condition must hold for precise rethrow to narrow the throws clause?The caught exception variable must not be reassigned in the block (it must be effectively final), so the compiler can prove it still holds one of the types thrown in the try.
- How do you call a SQLException-specific method inside catch (IOException | SQLException e)?Narrow it: if (e instanceof SQLException sql) { sql.getErrorCode(); } — the variable's static type is only the common supertype.
- Why does multi-catch forcing the variable final help precise rethrow?Precise rethrow needs the parameter effectively final; making multi-catch parameters implicitly final guarantees the analysis stays sound.
saying these in an interview costs you the question
- Thinking you can call subtype methods directly on the multi-catch variable
- Believing throw e; always rethrows the broad declared catch type
- Saying precise rethrow works even when you reassign the variable
- Confusing the variable's static type with its runtime type for method dispatch