Re-throwing and Exception Chaining
Selectively re-throw exceptions and wrap lower-level errors with cause chaining.
Re-throwing and Exception Chaining is a free Kotlin Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Re-throwing and Wrapping
Sometimes you catch an exception only to log, wrap, or convert it before letting a more specific exception propagate. Kotlin makes this idiomatic.
Plain Re-throw
To re-throw, just throw e again — propagation continues to the next handler.
fun main() {
try {
try { "x".toInt() }
catch (e: NumberFormatException) {
println("logging: ${e.message}")
throw e // re-throw original
}
} catch (e: NumberFormatException) {
println("outer caught: ${e.message}")
}
}Wrap with a Custom Exception
Pass the original exception as the cause when constructing a new one to preserve the chain.
class ParseException(msg: String, cause: Throwable) : RuntimeException(msg, cause)
fun main() {
try {
try { "xyz".toInt() }
catch (e: NumberFormatException) {
throw ParseException("could not parse user input", e)
}
} catch (e: ParseException) {
println("got: ${e.message}")
println("cause: ${e.cause?.message}")
}
}Inspecting the Cause Chain
The cause property links to the previous exception. Walk it to print the full root cause.
fun main() {
val e = RuntimeException("top",
RuntimeException("middle",
RuntimeException("root")))
var current: Throwable? = e
while (current != null) {
println(current.message)
current = current.cause
}
}Selective Re-throw
Catch a broad type, then re-throw if it does NOT match your expected case.
fun parseRequired(s: String): Int {
try {
return s.toInt()
} catch (e: NumberFormatException) {
if (s.isBlank()) return 0
throw e // unexpected — let it propagate
}
}
fun main() {
println(parseRequired("42"))
println(parseRequired(""))
// parseRequired("abc") // throws
}Adding Context
Wrap with a domain exception to add context that the original lacked (input, request id, etc).
class UploadError(msg: String, cause: Throwable) : RuntimeException(msg, cause)
fun upload(file: String) {
try { throw RuntimeException("disk full") }
catch (e: Throwable) { throw UploadError("uploading $file failed", e) }
}
fun main() {
try { upload("photo.jpg") }
catch (e: UploadError) { println("${e.message} <- ${e.cause?.message}") }
}Don't Lose the Stack Trace
When wrapping, always pass the original as cause — never just create a new exception with the same message but no cause. The stack trace is your debugging lifeline.
class Wrapped(m: String, cause: Throwable) : RuntimeException(m, cause)
fun main() {
try {
try { error("inner") }
catch (e: Throwable) { throw Wrapped("outer", e) }
} catch (e: Wrapped) {
println("message: ${e.message}")
println("cause: ${e.cause?.message}")
}
}Rethrow Without Wrapping
If logging is all you need, just throw e after the log call — no wrapping required.
fun risky() {
try { throw IllegalStateException("oops") }
catch (e: IllegalStateException) {
println("Logging before rethrow: ${e.message}")
throw e
}
}
fun main() {
try { risky() }
catch (e: IllegalStateException) { println("outer: ${e.message}") }
}finally for Cleanup During Rethrow
Use finally to clean up resources even when rethrowing.
fun process() {
try {
throw RuntimeException("failure")
} finally {
println("cleanup ran")
}
}
fun main() {
try { process() }
catch (e: RuntimeException) { println("caught: ${e.message}") }
}Suppressed Exceptions
If finally itself throws, the original exception is suppressed by default. Use addSuppressed to preserve both.
fun main() {
val primary = RuntimeException("primary")
val secondary = RuntimeException("from cleanup")
primary.addSuppressed(secondary)
println(primary.suppressed.joinToString { it.message ?: "?" })
}Anti-Pattern: Swallowing Then Throwing Different
Catching one exception and throwing an unrelated new one (without cause) destroys debugging info. Always pass cause.
// BAD
// catch (e: IOException) { throw RuntimeException("error") }
// GOOD
// catch (e: IOException) { throw RuntimeException("error reading config", e) }
fun main() { println("Pass cause when re-throwing!") }Quick Check
When you wrap an exception in a new one, what should you do to preserve the original stack trace?
Recap
To re-throw: throw e. To wrap: throw a new exception with cause = e. Always preserve the cause chain — never construct a wrapper with no cause. Use finally for cleanup, and addSuppressed when cleanup itself fails.
Frequently asked questions
Is the “Re-throwing and Exception Chaining” lesson free?
Yes — the full text of “Re-throwing and Exception Chaining” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.
What will I learn in “Re-throwing and Exception Chaining”?
Selectively re-throw exceptions and wrap lower-level errors with cause chaining. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Kotlin Academy?
No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Re-throwing and Exception Chaining” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Kotlin Academy lesson?
Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- try/catch/finally as an Expression
- Creating Custom Exception Classes
- runCatching and Result
- Re-throwing and Exception Chaining