Given that Kotlin removes checked-exception enforcement, how do you keep error handling robust at module boundaries in a mixed Kotlin/Java codebase?
answer
- Compiler enforces nothing — robustness is a design choice
- Result/runCatching/sealed for expected, recoverable errors
- Exceptions for truly exceptional, propagate to one boundary
- Never blanket-catch Throwable — it eats CancellationException
- @Throws + KDoc at Kotlin↔Java seams
basics
~20 sSince the compiler won't force you, you have to be disciplined: catch failures where you can act on them, model expected errors as return values (like a Result or sealed type), and document or annotate what each boundary can throw.
solid answer
~40 sBecause Kotlin enforces nothing, robustness becomes a design responsibility. Strategies: (1) model **expected, recoverable** failures as values — `kotlin.Result`, a `sealed class`/`sealed interface` for domain errors, or `runCatching {}` — so the type system carries the failure instead of the compiler. (2) Reserve **exceptions** for truly exceptional/unrecoverable cases and let them propagate to a single boundary handler. (3) At Kotlin↔Java seams, use `@Throws` so Java callers see the checked contract they expect, and document thrown types in KDoc. (4) Catch the **narrowest** type you can act on, never a blanket `catch (e: Throwable)` (which would also swallow `CancellationException`, breaking coroutine cancellation). (5) Centralize unexpected-error handling (e.g. a controller advice / top-level handler) so nothing is silently lost. The compiler's silence is traded for explicit error modeling.
code
kotlin · 6 linessuspend fun fetch(id: String): Result<User> = runCatching {
api.load(id) // may throw
}.recoverCatching { e ->
if (e is CancellationException) throw e // never swallow cancellation
throw DomainError("load failed for $id", e)
}go deeper
Understands that with no checked exceptions you must remember to handle failures yourself.
Knows to use try/catch where it matters and that exceptions still propagate; aware of Result/runCatching.
Designs with Result/sealed types, narrow catches, and boundary handlers; protects coroutine cancellation.
Architects a coherent error-handling strategy across a mixed Kotlin/Java codebase, balancing values-vs-exceptions, @Throws contracts, and centralized handling.
## The design shift Checked exceptions in Java were a (flawed) attempt to make the compiler enforce error handling. Kotlin removes that crutch, so **you** must architect robustness. The goal: failures are never silently lost, and recoverable errors are visible in the type system. ## 1. Model expected failures as values For errors a caller can sensibly react to, don't throw — return: ```kotlin sealed interface ParseResult { data class Ok(val value: Int) : ParseResult data class Invalid(val reason: String) : ParseResult } // or the stdlib Result type: fun parse(s: String): Result<Int> = runCatching { s.toInt() } ``` `kotlin.Result<T>` plus `runCatching { }`, `getOrElse`, `fold`, and `recover` let you treat failure as data. A `sealed` hierarchy gives exhaustive `when` handling with no `else` branch. ## 2. Reserve exceptions for the exceptional Programming errors and unrecoverable I/O failures should **throw** and propagate to a **single boundary handler** (a top-level try, a web `@ExceptionHandler`/controller advice, a coroutine `CoroutineExceptionHandler`). Don't scatter catch blocks that swallow and continue. ## 3. Honor the Java contract at the seam When Kotlin code is consumed by Java, add `@Throws(SomeChecked::class)` so the Java side gets the `throws` clause it relies on, and document throwable types in **KDoc** since Kotlin won't enforce them. ## 4. Catch narrowly; protect coroutine cancellation A blanket catch is dangerous: ```kotlin try { work() } catch (e: Throwable) { log(e) } // BUG: also catches CancellationException ``` Catching `Throwable` (or `Exception`) swallows `CancellationException`, which **breaks structured-concurrency cancellation**. Catch the specific type you can act on, and rethrow `CancellationException` if you must catch broadly: ```kotlin catch (e: CancellationException) { throw e } catch (e: IOException) { /* handle */ } ``` ## 5. Centralize the unexpected Give the application **one** place where uncaught failures are logged/translated (HTTP 500, error event, etc.). This compensates for the compiler not forcing local handling. ## Trade-off summary Kotlin trades compile-time checked-exception ceremony for **explicit error modeling**. Done well (Result/sealed + narrow catches + boundary handlers + `@Throws` at seams), it is more robust *and* less boilerplate than checked exceptions; done carelessly, failures vanish. ## Key keywords/APIs `kotlin.Result`, `runCatching`, `getOrElse`/`fold`/`recover`, `sealed class`/`sealed interface`, exhaustive `when`, `@Throws`, `CancellationException`, `CoroutineExceptionHandler`, KDoc `@throws`.
- Why is catching CancellationException broadly a problem in coroutines?Cancellation is signaled by throwing CancellationException; swallowing it makes a coroutine ignore cancellation, breaking structured concurrency and potentially leaking work.
- When prefer kotlin.Result over a custom sealed type?Result is convenient for a single success/failure with a Throwable. A sealed hierarchy is better when you have distinct, enumerable domain error cases you want exhaustively handled.
Java handed you a checklist at every door; Kotlin removed it, so you install one well-placed smoke detector (a boundary handler) and label the hazardous rooms (Result/sealed types) yourself.
saying these in an interview costs you the question
- Recommending blanket catch (e: Throwable) everywhere
- Ignoring CancellationException when catching broadly in coroutines
- Assuming the compiler will catch missing error handling
- Throwing exceptions for ordinary expected control flow
- Forgetting @Throws/KDoc at the Java interop boundary