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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Try it — edit, then press Run 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.

Implement it, then press Run to check 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 !!).

Implement it, then press Run to check 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