Kotlin runCatching Explained: A Cleaner Alternative to try-catch ?
Handling errors gracefully is a fundamental skill in building reliable Kotlin applications.
Most developers start by using the familiar try-catch
blocks — and while they work, they often lead to verbose, nested, and hard-to-read code.
Luckily, Kotlin offers a cleaner, more functional alternative: runCatching
.
In this guide, we'll start from the basics and progressively dive deeper. By the end, you’ll know when and why runCatching
is often the better choice.
Traditional Error Handling: try-catch
In Kotlin (and Java), we usually handle exceptions using try-catch
blocks.
fun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("Cannot divide by zero.")
0
}
}
fun main() {
println(divide(10, 2)) // Output: 5
println(divide(10, 0)) // Output: Cannot divide by zero. Then 0
}
While this works fine for small examples, it becomes increasingly bulky and error-prone as operations grow in complexity.
Introducing runCatching
Kotlin’s runCatching
function provides a functional and fluent way to handle exceptions.
It wraps risky code inside a block and returns a Result
object representing success or failure — without the need for explicit catch blocks.
A Simple Example
fun divide(a: Int, b: Int): Int {
return runCatching {
a / b
}.getOrElse {
println("Cannot divide by zero.")
0
}
}
fun main() {
println(divide(10, 2)) // Output: 5
println(divide(10, 0)) // Output: Cannot divide by zero. Then 0
}
Notice how the code is more concise and readable compared to traditional try-catch
.
Understanding the Result
Type
When you use runCatching
, it returns a Result
object that can be:
- Success(value) — the operation completed successfully
- Failure(exception) — the operation threw an exception
You can work with the result easily:
fun riskyOperation(): String {
if (Math.random() > 0.5) throw RuntimeException("Something went wrong!")
return "Success!"
}
fun main() {
val result = runCatching { riskyOperation() }
if (result.isSuccess) {
println("Yay! ${result.getOrNull()}")
} else {
println("Oops! ${result.exceptionOrNull()?.message}")
}
}
Scaling Up: Chaining Operations with runCatching
Real-world applications often require multiple operations that can fail.runCatching
shines here by letting you chain risky operations beautifully.
fun fetchDataFromServer(): String {
throw RuntimeException("Server not reachable")
}
fun parseData(data: String): Int {
return data.length
}
fun saveData(parsedData: Int) {
println("Saving data of length: $parsedData")
}
fun main() {
runCatching { fetchDataFromServer() }
.map { parseData(it) }
.onSuccess { saveData(it) }
.onFailure { println("Failed due to: ${it.message}") }
}
Instead of deeply nested try-catch blocks, you get a linear, readable flow of operations.
Bonus Tip: Recovering from Failures
You can also recover gracefully from a failure without throwing an error to your users.
val result = runCatching { "Kotlin".substring(10) } // Will throw
val finalResult = result.getOrDefault("Fallback value")
println(finalResult) // Output: Fallback value
Or using .recover()
for more flexible recovery:
val finalResult = result.recover { throwable ->
"Recovered from error: ${throwable.message}"
}.getOrNull()
println(finalResult)
When Should You Prefer runCatching
?
✅ When you want functional-style error handling.
✅ When you have multiple dependent risky operations.
✅ When you want to chain operations cleanly.
✅ When you prefer working with results instead of exceptions.
✅ When you need better readability and maintainability.
Final Thoughts
Traditional try-catch
blocks are reliable but can quickly clutter your code.
Kotlin’s runCatching
offers a more elegant, functional, and readable way to handle errors.
“If error handling is making your code messy, it’s a sign you should runCatching it.”
The next time you catch yourself writing nested try-catch
blocks, pause and think:
"Can I solve this better with runCatching
?"
Most often, the answer will be yes — and your codebase will thank you.