Choosing between exceptions, nullable returns and Result types shapes robust Kotlin APIs.
Kotlin uses exceptions like Java, with one big difference: there are no checked exceptions. You're never forced to catch or declare them (@Throws exists only for Java callers).
try is an expression:
val n = try { s.toInt() } catch (e: NumberFormatException) { 0 }throw is an expression of type Nothing, so it can be used in Elvis expressions: val user = find(id) ?: throw NotFoundException(id). finally runs regardless. For resources, use { } closes any Closeable automatically, like try-with-resources.
Preconditions from the standard library:
require(cond) { "message" }throwsIllegalArgumentExceptionfor bad arguments.check(cond)throwsIllegalStateExceptionfor bad state.requireNotNull(x)/checkNotNull(x)return the non-null value;error("msg")throwsIllegalStateException.
Alternatives to throwing:
- Return
nullfor "not found" or "can't parse" (toIntOrNull). - Return a sealed result type for domain errors with data.
runCatching { }wraps a block intoResult<T>, withgetOrNull(),getOrElse { },onFailure,map. Note it catches everything, including coroutineCancellationException, so avoid it around suspending code.
Use exceptions for programming errors and truly exceptional failures; use types for expected outcomes callers must handle.
Example
import java.io.StringReader
fun parsePort(s: String): Int {
val n = s.toIntOrNull() ?: throw IllegalArgumentException("not a number: $s")
require(n in 1..65535) { "port out of range: $n" }
return n
}
fun main() {
val port = try { parsePort("80x") } catch (e: IllegalArgumentException) {
println(e.message) // not a number: 80x
8080
}
println(port) // 8080
val r: Result<Int> = runCatching { parsePort("70000") }
println(r.isFailure) // true
println(r.exceptionOrNull()?.message) // port out of range: 70000
println(runCatching { parsePort("443") }.getOrElse { -1 }) // 443
val first = StringReader("line1\nline2").buffered().use { it.readLine() }
println(first) // line1 (reader closed)
var started = false
runCatching { check(started) { "not started" } }.onFailure { println(it.message) } // not started
}Remember
- No checked exceptions; try and throw are expressions
- require for arguments, check for state, error() to fail
- use { } closes resources
- Return null, sealed results or Result for expected failures
Watch out for
- Wrapping suspending code in runCatching, which swallows coroutine cancellation.