Error Handling
How Kotlin reports failure — the Result type and runCatching, the deliberate absence of checked exceptions, and when to model an expected error as a return value instead of throwing.
6 questions
JuniorTheoryVery commonDoes Kotlin have checked exceptions, and what is @Throws for then?
Does Kotlin have checked exceptions, and what is @Throws for then?
No. Every Kotlin exception is unchecked: nothing forces a caller to catch it or to list it in the signature, and the compiler never verifies that you did. @Throws exists purely for Java interop — it emits a throws clause so Java callers see it.
Common mistakes
- ✗Assuming Kotlin inherits Java's checked exceptions for anything deriving from
Exception - ✗Thinking
@Throwsforces Kotlin callers to catch the listed exceptions - ✗Believing an unannotated Kotlin function is therefore guaranteed not to throw
Follow-up questions
- →Why can a Java caller not write
catch (IOException e)around an unannotated Kotlin call? - →Does adding
@Throwschange anything for a Kotlin caller of the same function?
JuniorTheoryCommonWhat is Result<T> in Kotlin, and how do you read a value out of it?
What is Result<T> in Kotlin, and how do you read a value out of it?
Result<T> is one value that holds either a success value or the Throwable that failed. You inspect it with isSuccess, getOrNull() or fold(). Failure becomes an ordinary return value instead of a jump out of the call.
Common mistakes
- ✗Thinking
Result<T>drops theThrowableand only signals success or failure - ✗Believing a read of a failed
Resultrethrows instead of handing back the failure - ✗Assuming a compiler flag is needed before a function may return
Result<T>
Follow-up questions
- →What does
fold()give you that a plaingetOrNull()check does not? - →How is
Result<T>different from asealederror type you declare yourself?
MiddleTheoryCommonWhat is the trade-off of Kotlin having no checked exceptions at all?
What is the trade-off of Kotlin having no checked exceptions at all?
You gain: no throws boilerplate, no catch-and-ignore ceremony. You lose the compiler telling you a call can fail — the signature is silent. Recoverable failures are modelled by hand as a Result or a sealed error type; the author documents what throws.
Common mistakes
- ✗Thinking Kotlin still checks anything deriving from
Exception, the way Java does - ✗Expecting the compiler to warn at a call site that a function can throw
- ✗Assuming
runCatchingaround every call restores the lost compile-time guarantee
Follow-up questions
- →When is a
sealederror type a better answer here than returningResult<T>? - →What should a KDoc
@throwstag say for a function with no@Throwson it?
MiddleDebuggingCommonWhy does this coroutine keep running after its scope was cancelled?
Why does this coroutine keep running after its scope was cancelled?
runCatching catches Throwable, so it also swallows the CancellationException that cancellation throws: the coroutine reads cancellation as an ordinary failure, logs it and carries on. Rethrow it, or catch only the exceptions you can act on.
Common mistakes
- ✗Thinking
runCatchingcatches onlyExceptionand leavesCancellationExceptionalone - ✗Treating a caught
CancellationExceptionas a real failure worth logging and retrying - ✗Assuming
getOrNull()on a failedResultrethrows and thereby stops the coroutine
Follow-up questions
- →Why does the same bug show up with a bare
try/catch (e: Throwable)? - →What does calling
coroutineContext.ensureActive()insideonFailurechange here?
MiddleDesignOccasionalYou are designing the API of a payment module for an Android app. Its charge() call can fail in three ways: the card details fail validation, the network call times out, and — separately — a caller passes a negative amount, which is a bug in the calling code. The team's requirement is that a failure the UI must react to should be impossible for a caller to forget, while a genuine programming error should surface loudly in crash reporting rather than be quietly handled. The module is consumed only from Kotlin. Decide, for each of the three failures, whether it should be a value returned from charge() or a thrown exception; say what type the returned failure should carry; and justify the split.
You are designing the API of a payment module for an Android app. Its charge() call can fail in three ways: the card details fail validation, the network call times out, and — separately — a caller passes a negative amount, which is a bug in the calling code. The team's requirement is that a failure the UI must react to should be impossible for a caller to forget, while a genuine programming error should surface loudly in crash reporting rather than be quietly handled. The module is consumed only from Kotlin. Decide, for each of the three failures, whether it should be a value returned from charge() or a thrown exception; say what type the returned failure should carry; and justify the split.
Validation and timeout are expected, recoverable failures: return them as a value the caller cannot ignore. A sealed error type beats Result<T> — it names each case and a when over it is exhaustive. The negative amount is a bug: throw it, so it surfaces in crash reporting.
Common mistakes
- ✗Modelling a programming error as a recoverable failure value instead of throwing it
- ✗Reaching for
Result<T>where a sealed type naming each failure case fits better - ✗Expecting
@Throwsto force a Kotlin caller to handle a thrown failure
Follow-up questions
- →How would your split change if the module were also consumed from Java?
- →Why is an exhaustive
whenover a sealed error type stronger than anisFailurecheck?
SeniorDesignRareYou own a shared Kotlin library — an HTTP client wrapper — published to both Kotlin and Java consumers. Its fetch() can fail on a malformed URL, on a 4xx/5xx response, and on an I/O timeout, and consumers must be able to tell those apart. A teammate proposes returning Result<Response> from every public function and wrapping each implementation body in runCatching so that nothing ever escapes the library boundary. The public API has to stay usable and honest from Java, with no Kotlin-only helper layer bolted on top. Design the library's public error surface: say what fetch() should hand back, what a Java caller actually sees of it at compile time, and what each half of your teammate's proposal breaks.
You own a shared Kotlin library — an HTTP client wrapper — published to both Kotlin and Java consumers. Its fetch() can fail on a malformed URL, on a 4xx/5xx response, and on an I/O timeout, and consumers must be able to tell those apart. A teammate proposes returning Result<Response> from every public function and wrapping each implementation body in runCatching so that nothing ever escapes the library boundary. The public API has to stay usable and honest from Java, with no Kotlin-only helper layer bolted on top. Design the library's public error surface: say what fetch() should hand back, what a Java caller actually sees of it at compile time, and what each half of your teammate's proposal breaks.
Return a sealed hierarchy of named domain errors from fetch(): the compiler forces an exhaustive when. Result<T> is an inline value class, so Java sees a mangled, boxed signature; Java gets a compile-time signal only where you throw with @Throws. Never blanket-wrap a call in runCatching.
Common mistakes
- ✗Exposing
Result<T>across the Java boundary and expecting a clean, readable signature - ✗Wrapping every public call in
runCatching, swallowing cancellation and genuine bugs alike - ✗Assuming Java callers get a compile-time signal without a thrown,
@Throws-annotated failure
Follow-up questions
- →How do you keep such a sealed error hierarchy source-compatible as you add new cases?
- →What does
@Throws(IOException::class)actually generate for a Java caller of that function?