Exceptions Workbook
Ten short exercises on Kotlin exception handling — try/catch as an expression, require and check, use for resources, runCatching and Result, Nothing, and sealed result types.
Practice problems for Unchecked and Unbothered. Each takes a minute or two. Each exercise has a runnable editor — fill in the TODO, press Run to try it (the code runs on JetBrains’ Kotlin compiler server), then click Show answer to check yourself. Nothing here is a trick question, just direct practice of the syntax from the lesson.
try and preconditions
1. try as an expression
Assign number the result of parsing text as an Int, or 0 if it isn’t a valid number — using try/catch as an expression.
fun main() {
val text = "42x"
val number = 0 // TODO: parse text to Int, or 0 if invalid — use try/catch
println(number)
} Show answer Hide answer
val number = try {
text.toInt()
} catch (e: NumberFormatException) {
0
} 2. Require an argument
Write sqrt(x: Double) that requires x >= 0, throwing IllegalArgumentException with a message otherwise.
fun main() {
fun sqrt(x: Double): Double {
// TODO: require x >= 0, else IllegalArgumentException with a message
return Math.sqrt(x)
}
println(sqrt(9.0))
} Show answer Hide answer
fun sqrt(x: Double): Double {
require(x >= 0) { "x must be non-negative" }
return Math.sqrt(x)
} 3. Check state
Inside a method, throw IllegalStateException when isClosed is true, using the precondition helper for state.
fun main() {
val isClosed = true
try {
// TODO: throw IllegalStateException when isClosed, using check()
println("still open")
} catch (e: IllegalStateException) {
println("caught: " + e.message)
}
} Show answer Hide answer
check(!isClosed) { "already closed" }require is for arguments; check is for state.
resources and Result
4. Auto-close a resource
Read all text from a reader (an AutoCloseable), making sure it’s closed afterward.
fun main() {
class Reader : AutoCloseable {
fun readText() = "file contents"
override fun close() = println("closed")
}
val reader = Reader()
val text = "" // TODO: read all text via reader.use { }, closing afterward
println(text)
} Show answer Hide answer
reader.use { it.readText() }use closes the resource whether the block succeeds or throws — Kotlin’s try-with-resources.
5. Capture success or failure
Use runCatching to parse text to an Int, returning -1 on failure.
fun main() {
val text = "oops"
val number = -1 // TODO: runCatching to parse text to Int, -1 on failure
println(number)
} Show answer Hide answer
runCatching { text.toInt() }.getOrElse { -1 } 6. A function that always throws
Write fail(message: String) whose return type tells the compiler control never continues past it.
fun main() {
fun fail(message: String) { // TODO: give this a return type of Nothing
throw IllegalStateException(message)
}
try {
fail("boom")
} catch (e: IllegalStateException) {
println("caught: " + e.message)
}
} Show answer Hide answer
fun fail(message: String): Nothing = throw IllegalStateException(message)Nothing lets it stand in anywhere — e.g. val x = maybe ?: fail("missing").
the bigger picture
7. No checked exceptions
readFile() can throw IOException. Write a function that calls it and returns the result — with no try/catch and no throws clause.
fun main() {
fun readFile(): String = "file contents" // may throw IOException in real life
fun load(): String {
return "" // TODO: call readFile() and return it — no try/catch, no throws
}
println(load())
} Show answer Hide answer
fun load(): String {
return readFile() // no catch, no throws — Kotlin has no checked exceptions
} 8. Model errors as data
For a fetch that can fail in an expected way, declare a sealed FetchResult with Success(val user: User) and Error(val message: String) instead of throwing.
class User(val name: String)
// TODO: give FetchResult a Success(val user: User) and an Error(val message: String) subclass
sealed class FetchResult
fun main() {
val result: FetchResult? = null
println("FetchResult is ready to model success and error: " + result)
} Show answer Hide answer
sealed class FetchResult {
data class Success(val user: User) : FetchResult()
data class Error(val message: String) : FetchResult()
}Sealed result types make the failure path part of the return type, checked by an exhaustive when.
9. Get-or-null
Use runCatching to attempt risky() and produce its result or null.
fun main() {
fun risky(): Int = 42
val result: Int? = null // TODO: runCatching { risky() } producing its result or null
println(result)
} Show answer Hide answer
runCatching { risky() }.getOrNull() 10. Catch is an expression too
Assign config from loadConfig(), falling back to Config.default() if it throws — in one expression.
fun main() {
class Config(val name: String) {
companion object {
fun default() = Config("default")
}
}
fun loadConfig(): Config = throw RuntimeException("no config on disk")
val config = Config.default() // TODO: try loadConfig(), fall back to Config.default()
println(config.name)
} Show answer Hide answer
val config = try { loadConfig() } catch (e: Exception) { Config.default() } Going deeper: try as an expression, and Result
12. Assign from a try
try is an expression. Implement parseOrZero so it returns the parsed Int, or 0 when the string isn’t a number — assigning the try directly.
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun parse() {
Assert.assertEquals(42, parseOrZero("42"))
Assert.assertEquals(0, parseOrZero("nope"))
}
}
//sampleStart
fun parseOrZero(s: String): Int =
0 // TODO: return s.toInt(), or 0 on NumberFormatException — as a try expression
//sampleEnd Show answer Hide answer
fun parseOrZero(s: String): Int = try {
s.toInt()
} catch (e: NumberFormatException) {
0
}Because try yields a value, the result is a val with no mutable placeholder — impossible in Java’s statement-only try.
13. Fold failure into a value
Use runCatching so safeLength returns the string’s length, or -1 if the string is null (and you dereference it with !!).
import org.junit.Test
import org.junit.Assert
class Test {
@Test fun safeLength() {
Assert.assertEquals(3, safeLength("Ada"))
Assert.assertEquals(-1, safeLength(null))
}
}
//sampleStart
fun safeLength(s: String?): Int =
0 // TODO: runCatching { s!!.length }, defaulting to -1
//sampleEnd Show answer Hide answer
fun safeLength(s: String?): Int = runCatching { s!!.length }.getOrDefault(-1)runCatching turns a throwing block into a Result<T>. Handy at boundaries — but it catches all Throwable, so don’t wrap ordinary control flow in it (and never around coroutine code, where it would swallow CancellationException).
14. Which way needs @Throws?
Kotlin has no checked exceptions. When you call a Java method that declares throws IOException, must you catch it? And when Java calls your Kotlin function that throws IOException, what do you add so Java can catch it? Reveal to check.
Show answer Hide answer
Calling Java from Kotlin: no — Kotlin ignores the throws clause, so you may catch it but aren’t forced to (the exception just propagates). Java calling Kotlin: add @Throws(IOException::class), which writes the Exceptions metadata Java’s compiler needs so its catch isn’t flagged as unreachable.
Back to the lesson, Unchecked and Unbothered, or on to the next one: Java interop.
Comments