You own a Kotlin library consumed by both Kotlin and Java teams. Design a policy for using @Throws across the public API, addressing compatibility, documentation, and the trade-offs of exposing checked-exception contracts.
answer
- throws clause = part of the ABI/API contract
- Adding or removing @Throws is source-breaking for Java
- Annotate only public Java-facing surface
- Pair with KDoc @throws; validate signatures in CI
- Keep checked exceptions meaningful and stable
basics
~20 sAdd @Throws only on public methods Java teams call that genuinely throw a checked exception worth handling. Treat the throws clause as part of your binary contract: adding/removing it can break Java callers. Document it and keep it stable.
solid answer
~50 sPolicy: scope `@Throws` to the **public, Java-facing surface** only, and treat each `throws` clause as part of the **API contract**. Adding a checked exception forces Java callers to handle it (source-breaking); removing one can break Java callers that declared or caught it (source-breaking, and a behavioral signal). So changes to `@Throws` should go through the same review as signature changes and follow semantic-versioning rules. Prefer declaring **stable, meaningful** checked exceptions (`IOException`) rather than over-specifying. For exceptions Java callers cannot reasonably recover from, prefer unchecked types and omit `@Throws`, keeping the surface clean. Pair every `@Throws` with KDoc `@throws`/`@exception` so both Kotlin and Java consumers see intent. Use binary-compatibility tooling (e.g. the Kotlin `binary-compatibility-validator`) and tests that assert the Java-visible signatures, so an accidental annotation change is caught in CI rather than by downstream Java teams.
go deeper
Knows @Throws is for Java interop and should be on public methods that throw checked exceptions.
Recognizes that the throws clause is part of the API and that changes affect Java callers.
Specifies which exceptions to expose, pairs with KDoc, and limits @Throws to the Java-facing surface.
Designs a full governance policy: semver impact of add/remove, CI binary-compatibility validation, override rules, and the unchecked-vs-checked decision boundary.
## The core tension Kotlin chose to drop checked exceptions, but a library serving **Java** teams lives partly in Java's world. `@Throws` is the only lever to expose checked-exception contracts. Because the `throws` clause becomes part of the **JVM method signature**, it is effectively part of your **public API**, with compatibility consequences. ## Compatibility rules to encode - **Adding `@Throws(SomeChecked)`** to an existing method is **source-breaking for Java**: callers that did not handle it must now catch or declare it. Treat as a breaking change. - **Removing `@Throws`** can be **source-breaking for Java** too: code that declared `throws IOException` or caught it may now fail (*'exception never thrown'*). It is also a **behavioral signal** that the method no longer intends to throw that type. - **Kotlin callers** are unaffected either way — but you still cannot ignore the Java side. - Run a **binary-compatibility validator** in CI and, ideally, **golden tests on the Java-visible signatures**, so annotation drift is caught early. ## Design guidelines 1. **Annotate only the public, Java-facing API.** Internal Kotlin-only code should not carry `@Throws` — it is noise and a future compatibility liability. 2. **Expose only meaningful, recoverable checked exceptions.** I/O and parse failures Java callers can sensibly handle are good candidates. Programming errors should stay **unchecked** and undeclared. 3. **Be precise, not exhaustive.** Listing every conceivable subtype forces dead `catch` blocks on Java callers. List the contract-level type (e.g. `IOException`) the caller should handle. 4. **Document in KDoc.** Always pair `@Throws` with a KDoc `@throws Type description` so intent is visible to both ecosystems and generated docs. 5. **Keep it stable.** Once published, a `throws` clause is a promise; change it only with a version bump and changelog entry. 6. **Consider the override rule.** Since `@Throws` is not inherited, decide whether public overridable methods need the annotation repeated on implementations callers might bind to statically. ## Example contract ```kotlin import java.io.IOException /** * Reads and parses the config file. * @throws IOException if the file cannot be read. */ @Throws(IOException::class) fun loadConfig(path: String): Config { /* ... */ TODO() } ``` ## When NOT to use it - Pure-Kotlin libraries with no Java consumers: skip it entirely. - Failures that are programmer errors (`IllegalArgumentException`, `IllegalStateException`): leave unchecked; documenting via KDoc is enough. ## Governance summary Treat `@Throws` like any other part of the ABI: minimal, intentional, documented, version-controlled, and CI-validated. The goal is to give Java teams ergonomic, stable checked-exception handling without polluting the Kotlin experience or accumulating compatibility debt.
- Why is removing @Throws potentially breaking even though it 'relaxes' the contract?Java callers that declared 'throws IOException' or wrapped the call in a catch may fail to compile with 'exception never thrown' once the clause disappears, so it is a source-compatibility break.
- How would you catch accidental @Throws changes before release?Run a binary-compatibility validator and/or golden tests asserting the Java-visible signatures in CI, plus require signature-level review for any annotation change.
saying these in an interview costs you the question
- Treating @Throws as a free, cost-less annotation to add anywhere
- Ignoring that removing it can break Java callers
- Declaring unchecked/programmer-error exceptions via @Throws
- Not documenting with KDoc @throws
- No CI guard against signature/annotation drift