skip to content

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.

level: principalimportance: nice to knowfreq 14%

answer

  1. throws clause = part of the ABI/API contract
  2. Adding or removing @Throws is source-breaking for Java
  3. Annotate only public Java-facing surface
  4. Pair with KDoc @throws; validate signatures in CI
  5. Keep checked exceptions meaningful and stable

basics

~20 s

Add @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 s

Policy: 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

for a junior

Knows @Throws is for Java interop and should be on public methods that throw checked exceptions.

for a middle

Recognizes that the throws clause is part of the API and that changes affect Java callers.

for a senior

Specifies which exceptions to expose, pairs with KDoc, and limits @Throws to the Java-facing surface.

for a principal

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

context