skip to content

Compare @Throws semantics on Kotlin/Native versus the JVM. Why can the same annotation behave so differently?

level: middleimportance: should knowfreq 40%

answer

  1. JVM: throws-clause for Java callers only, inert for Kotlin
  2. Native: load-bearing, opens the NSError channel
  3. Native unlisted → process abort; JVM unlisted → still catchable
  4. JVM shares exception runtime; ObjC/Swift does not
  5. Kotlin never gets compile-time checked exceptions

basics

~10 s

On the JVM, @Throws only adds a note for Java callers and changes nothing for Kotlin. On Native, it actually controls which exceptions can safely cross into Swift; unlisted ones crash.

solid answer

~40 s

`@Throws` is one annotation with target-specific compiler behavior. On the **JVM**, Kotlin has no checked exceptions, so `@Throws` only emits a `throws` clause into the method's bytecode signature so **Java** callers see a checked exception; it imposes no requirement on Kotlin callers and does not change runtime unwinding — any exception can still be caught anywhere. On **Kotlin/Native**, exported functions are non-throwing by default, so `@Throws` is **load-bearing**: it generates the `NSError**` channel and declares exactly which exception types may bridge to Objective-C/Swift. Listed types bridge; unlisted ones hit the boundary as unhandled and **terminate the process**. The difference exists because the JVM already has a universal exception model shared by Kotlin/Java, whereas the ObjC/Swift boundary has none, so the annotation must do real marshalling work there.

go deeper

for a junior

Knows @Throws matters more on iOS/Native than on Android/JVM.

for a middle

Explains the JVM Java-interop throws clause versus the Native NSError marshalling, and why unlisted differs.

for a senior

Reasons about a shared commonMain API where the same annotation has benign JVM and safety-critical Native effects.

for a principal

Sets multiplatform API conventions so iOS safety is guaranteed without burdening the JVM target or misusing the annotation as type enforcement.

## Same annotation, two jobs `kotlin.Throws` is multiplatform, but the compiler backend for each target interprets it differently. ### On the JVM - Kotlin **does not have checked exceptions**. You can throw anything without declaring it. - `@Throws(IOException::class)` exists purely for **Java interop**: it writes a `throws IOException` clause into the compiled method signature so Java callers are forced (by the Java compiler) to handle or declare it. - For **Kotlin callers** it is inert — no compile-time check, no runtime change. Unwinding is the normal JVM model: any `throw` propagates and is catchable anywhere up the stack. ### On Kotlin/Native - Functions exported to Objective-C/Swift are generated as **non-throwing** by default. - `@Throws` is the **only** way to open an error channel: it emits the `error:(NSError**)` parameter and declares the bridgeable set. - **Listed** exception types (and subclasses) → wrapped into `NSError` → Swift `throws`. - **Unlisted** exception types reaching the boundary → `processUnhandledException` → **process abort**. ## Why the divergence The JVM gives Kotlin and Java a **shared exception runtime**, so nothing special is needed to cross between them — the annotation is just documentation/interop sugar. The **ObjC/Swift** boundary has **no exception runtime in common** with Kotlin; failures there must be reified into `NSError`. So on Native the annotation has to actually decide and implement the marshalling contract. ## Practical consequences - A multiplatform `commonMain` function with `@Throws` is harmless on JVM/Android but **safety-critical** on iOS — it determines crash-vs-recoverable behavior. - Putting `@Throws` only where Swift consumes the API keeps the iOS surface safe without affecting Android. ```kotlin // commonMain @Throws(ApiException::class) fun fetch(id: String): Dto // Android: just a throws-clause for any Java callers. // iOS: ApiException bridges to Swift; anything else crashes. ``` ## Edge note `@Throws` does not make exceptions **checked** for Kotlin anywhere; Kotlin remains uncheck-exception throughout. It only affects the cross-language signature/marshalling.

  • Does @Throws ever make Kotlin enforce checked exceptions at compile time?
    No. Kotlin has no checked exceptions on any target. @Throws only affects interop signatures (JVM->Java) and Native boundary marshalling.
  • Is a @Throws in commonMain dangerous for the Android build?
    No. On Android/JVM it is inert for Kotlin callers and only adds a Java-visible throws clause; the crash-vs-bridge semantics apply only on Native.

saying these in an interview costs you the question

  • Saying @Throws enforces checked exceptions in Kotlin
  • Claiming behavior is identical on JVM and Native
  • Thinking the JVM aborts on an unlisted exception like Native does
  • Believing @Throws on JVM affects Kotlin-to-Kotlin calls
  • Not knowing the JVM clause is for Java interop

context