Step 9 of 13
try/catch/finally, custom exceptions, Result type with runCatching, sealed error types, coroutine exceptions
การจัดการ error — try/catch, Result type, sealed errors, และ exception ใน coroutines
Kotlin exception handling looks like Java:
fun parseNumber(input: String): Int {
return try {
input.toInt()
} catch (e: NumberFormatException) {
println("Invalid number: $input")
0
} finally {
println("Parse attempt finished")
}
}
fun main() {
println(parseNumber("42")) // 42
println(parseNumber("abc")) // 0
}
try is also an expression — it returns a value:
val result: Int = try {
input.toInt()
} catch (e: NumberFormatException) {
-1
}
fun withdraw(balance: Double, amount: Double): Double {
if (amount > balance) {
throw IllegalArgumentException("Insufficient funds: have $balance, need $amount")
}
return balance - amount
}
try {
withdraw(100.0, 150.0)
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
// Error: Insufficient funds: have 100.0, need 150.0
}
class InsufficientFundsException(balance: Double, amount: Double) :
Exception("Have $$balance but need $$amount")
fun withdraw(balance: Double, amount: Double): Double {
if (amount > balance) {
throw InsufficientFundsException(balance, amount)
}
return balance - amount
}
Instead of throwing, return a Result<T>. It either holds a success value or an exception:
import java.io.File
fun readFile(path: String): Result<String> = runCatching {
File(path).readText()
}
fun main() {
val result = readFile("config.txt")
// Check and extract
if (result.isSuccess) {
println("Content: ${result.getOrNull()}")
} else {
println("Error: ${result.exceptionOrNull()?.message}")
}
// Or use getOrElse
val content = result.getOrElse { "default content" }
println(content)
// Or use fold for both branches
result.fold(
onSuccess = { println("Read OK: ${it.length} chars") },
onFailure = { println("Read failed: ${it.message}") }
)
}
Use sealed classes for typed errors:
sealed class ParseError {
data object EmptyInput : ParseError()
data class InvalidFormat(val raw: String) : ParseError()
data class OutOfRange(val value: Int) : ParseError()
}
fun parseScore(input: String): Result<Int> {
if (input.isBlank()) {
return Result.failure(ParseError.EmptyInput)
}
val value = input.toIntOrNull()
?: return Result.failure(ParseError.InvalidFormat(input))
if (value !in 0..100) {
return Result.failure(ParseError.OutOfRange(value))
}
return Result.success(value)
}
fun main() {
listOf("85", "abc", "150", "")
.map { input -> input to parseScore(input) }
.forEach { (input, result) ->
val message = result.fold(
onSuccess = { "OK: $it" },
onFailure = { err ->
when (err) {
is ParseError.EmptyInput -> "empty"
is ParseError.InvalidFormat -> "invalid: ${err.raw}"
is ParseError.OutOfRange -> "out of range: ${err.value}"
}
}
)
println("\"$input\" → $message")
}
// "85" → OK: 85
// "abc" → invalid: abc
// "150" → out of range: 150
// "" → empty
}
Coroutines have special rules for exceptions:
import kotlinx.coroutines.*
import kotlin.time.Duration.Companion.milliseconds
// Method 1: try/catch inside the coroutine
suspend fun main() = coroutineScope {
launch {
try {
delay(100.milliseconds)
throw RuntimeException("Something went wrong")
} catch (e: Exception) {
println("Caught: ${e.message}")
}
}
}
// Caught: Something went wrong
By default, if one child coroutine fails, all siblings are cancelled. Use supervisorScope to let children fail independently:
suspend fun main() = supervisorScope {
launch {
delay(100.milliseconds)
throw RuntimeException("Child 1 failed")
}
launch {
delay(200.milliseconds)
println("Child 2 still running!") // This still runs
}
}
| Scenario | Approach |
|---|---|
| Input validation (user data) | Return Result<T> |
| Unreachable state (bug) | Throw exception |
| External system failure (network) | Return Result<T> |
| Programming error (null where impossible) | Throw exception |
Rule of thumb: If the caller can reasonably handle the failure, return Result. If the failure means the program state is broken, throw.
Build a safe user registration flow using Result:
data class User(val email: String, val name: String)
sealed class RegistrationError {
data object EmptyName : RegistrationError()
data class InvalidEmail(val email: String) : RegistrationError()
data object DuplicateUser : RegistrationError()
}
private val existingEmails = mutableSetOf("taken@email.com")
fun register(email: String, name: String): Result<User> {
if (name.isBlank()) return Result.failure(RegistrationError.EmptyName)
if (!email.contains("@")) return Result.failure(RegistrationError.InvalidEmail(email))
if (email in existingEmails) return Result.failure(RegistrationError.DuplicateUser)
existingEmails.add(email)
return Result.success(User(email, name))
}
fun main() {
val requests = listOf(
"alice@email.com" to "Alice",
"taken@email.com" to "Bob",
"bad-email" to "Charlie",
"new@email.com" to "",
)
requests.forEach { (email, name) ->
val message = register(email, name).fold(
onSuccess = { "Registered: ${it.name}" },
onFailure = { err ->
when (err) {
RegistrationError.EmptyName -> "Name cannot be empty"
is RegistrationError.InvalidEmail -> "Bad email: ${err.email}"
RegistrationError.DuplicateUser -> "Email already taken"
}
}
)
println("$email → $message")
}
}