Result & runCatching
Reviewed & published by Brayan K
Result<T> is a built-in type that holds either a success value or a captured exception, letting you treat failure as a value you handle rather than an exception that unwinds the stack.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll capture outcomes with runCatching, extract values with getOrNull/getOrElse, and transform with map/fold.
What You'll Learn in This Lesson
- What Result<T> models and why it helps
- Capturing success or failure with runCatching
- getOrNull, getOrDefault, and getOrElse
- Transforming with map and collapsing with fold
- Side effects with onSuccess and onFailure
- When to use Result vs throwing exceptions
1️⃣ runCatching and Result Basics
runCatching { ... } runs the block and returns a Result<T>: a success holding the value, or a failure holding the thrown exception. Check it with isSuccess / isFailure, and read the value with getOrNull().
// runCatching runs a block and captures success OR failure
// into a Result<T> instead of throwing.
fun main() {
// Success case: the block returns a value
val good: Result<Int> = runCatching { 10 / 2 }
println(good.isSuccess) // -> true
println(good.getOrNull()) // -> 5
// Failure case: the block throws; the exception is captured
val bad: Result<Int> = runCatching { 10 / 0 }
println(bad.isFailure) // -> true
println(bad.getOrNull()) // -> null (no value)
}2️⃣ Extracting the Value Safely
There are three common extractors. getOrNull() gives the value or null; getOrDefault(x) gives the value or a constant; getOrElse { e -> ... } gives the value or computes a fallback from the captured exception.
// Several ways to pull a value out safely.
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
fun main() {
val ok = parse("42")
val no = parse("oops")
// getOrNull: value or null
println(ok.getOrNull()) // -> 42
println(no.getOrNull()) // -> null
// getOrDefault: value or a fallback
println(no.getOrDefault(-1)) // -> -1
// getOrElse: value or compute from the exception
println(no.getOrElse { 0 }) // -> 0
}3️⃣ Transforming with map and fold
map transforms the success value and passes failures through untouched, perfect for a happy-path pipeline. fold collapses both branches into one value. onSuccess / onFailure run side effects and return the same Result for chaining.
// map transforms the success value; failures pass through untouched.
// fold handles BOTH branches in one call.
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
fun main() {
// map: double the number if it parsed
val doubled = parse("21").map { it * 2 }
println(doubled.getOrNull()) // -> 42
// fold: one expression for success and failure
val message = parse("oops").fold(
onSuccess = { "Got $it" },
onFailure = { "Failed" }
)
println(message) // -> Failed
// onSuccess / onFailure run side effects and return the Result
parse("7")
.onSuccess { println("ok: $it") } // -> ok: 7
.onFailure { println("bad") } // (not called)
}Your turn. Fill in the ___ blanks, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
val result = runCatching { "100".toInt() }
// 1) Get the value or -1 if it failed
val value = result.___(-1) // 👉 put: getOrDefault
// 2) Double it only on success using map, then read with getOrNull
val doubled = result.___ { it * 2 }.getOrNull() // 👉 put: map
println("$value $doubled")
// ✅ Expected output:
// 100 200
}Mini-Challenge: Safe Division Reporter
Use runCatching and fold to report a division that might divide by zero.
// 🎯 MINI-CHALLENGE: safe division reporter.
// Save as SafeDivide.kt and run main().
//
// 1. Write divide(a, b) returning Result<Int> via runCatching { a / b }.
// 2. In main(), call divide(10, 2) and divide(10, 0).
// 3. For each, use fold to print "Result: <n>" on success
// or "Error" on failure.
//
// ✅ Expected output:
// Result: 5
// Error
fun divide(a: Int, b: Int): Result<Int> = runCatching { a / b }
fun report(r: Result<Int>): String {
// use r.fold(onSuccess = ..., onFailure = ...)
return ""
}
fun main() {
println(report(divide(10, 2)))
println(report(divide(10, 0)))
}Common Errors (and the fix)
- ❌ Swallowing real bugs — wrapping everything in runCatching hides logic errors. ✅ Use it only for failures you plan to handle.
- ❌ Calling getOrThrow() defeats the purpose — you're back to exceptions. ✅ Prefer getOrElse or fold to handle failure.
- ❌ Ignoring the failure branch — only checking getOrNull() may lose the error. ✅ Use fold or onFailure when the reason matters.
- ❌ Using map for a throwing transform — it won't catch the new throw. ✅ Use mapCatching when the transform can fail.
Pro Tips
- 💡 Chain it: runCatching { ... }.map { ... }.getOrElse { ... } reads as one clean pipeline.
- 💡 Receiver form: value.runCatching { ... } makes this the value inside the block.
- 💡 Don't catch what you can't handle: let truly unexpected exceptions (bugs) crash during development.
📋 Quick Reference — Result API
| Need | Call |
|---|---|
| Capture outcome | runCatching { ... } |
| Value or null | getOrNull() |
| Value or constant | getOrDefault(x) |
| Value or computed | getOrElse { e -> ... } |
| Transform success | map { ... } |
| Handle both | fold(onSuccess, onFailure) |
🎉 Lesson Complete!
- ✅ Result<T> holds a success value or a captured exception
- ✅ runCatching turns a throwing block into a Result
- ✅ getOrNull/getOrDefault/getOrElse extract safely
- ✅ map transforms success; fold handles both branches
- ✅ Use Result for expected failures, exceptions for bugs
Practice quiz
What does a Result<T> hold?
- Always both a value and an exception
- Either a success value or a captured exception, never both
- Only a success value
- A list of results
Answer: Either a success value or a captured exception, never both. Result<T> models exactly one of two outcomes: success value or captured failure.
What does runCatching { } return?
- The raw value or it throws
- A Boolean for success
- A Result wrapping success or failure
- Always null on error
Answer: A Result wrapping success or failure. runCatching runs the block and wraps the outcome in a Result.
If the block in runCatching throws, what do you get?
- The exception propagates immediately
- Result.failure(exception)
- Result.success(null)
- A compile error
Answer: Result.failure(exception). runCatching catches the throw and returns Result.failure(exception).
What does getOrNull() return on a failed Result?
- The exception
- An empty string
- Zero
- null
Answer: null. getOrNull() returns the value on success or null on failure.
What does getOrDefault(-1) return when the Result is a failure?
- -1
- null
- The exception
- It throws
Answer: -1. getOrDefault returns the supplied constant fallback on failure.
What does getOrElse { } give you access to in its lambda?
- The success value
- The captured exception, to compute a fallback
- Nothing, it takes no parameter
- The original Result
Answer: The captured exception, to compute a fallback. getOrElse receives the captured exception so the fallback can depend on the error.
What does map do to a failed Result?
- Throws the exception
- Converts it to success
- Leaves the failure untouched and passes it through
- Returns null
Answer: Leaves the failure untouched and passes it through. map transforms only the success value; failures pass through unchanged.
What does fold(onSuccess, onFailure) do?
- Combines two Results
- Collapses the Result into one value by handling both branches
- Only handles the success branch
- Throws on failure
Answer: Collapses the Result into one value by handling both branches. fold produces a single value by handling success and failure in one call.
Which should you use for a transform that might itself throw?
- map
- fold
- getOrNull
- mapCatching
Answer: mapCatching. mapCatching catches exceptions thrown inside the transform; plain map does not.
When is Result the right tool instead of throwing?
- For all exceptions, always
- For expected, recoverable failures the caller should handle
- Only for programmer errors
- Never; always throw
Answer: For expected, recoverable failures the caller should handle. Use Result for expected failures; keep throwing for true bugs that should crash loudly.
Continue this course
- Previous: Building DSLs (Lambdas with Receiver)
- Next: Advanced Collections (groupBy, fold, windowed) — Group, aggregate, chunk, and zip collections like a pro
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What is Result<T> in Kotlin?
Result<T> is a built-in type that holds EITHER a success value of type T OR a captured exception, never both. It turns error handling from control flow (throwing and catching) into a value you can pass around, transform, and inspect. You check it with isSuccess and isFailure, extract the value with functions like getOrNull, and combine it with map and fold. It is especially handy for operations that might fail in expected ways — parsing input, reading a file, calling a network — where you want to handle the failure explicitly rather than let an exception unwind the stack.
What does runCatching do?
runCatching executes the block you give it and wraps the outcome in a Result. If the block returns normally, you get Result.success(value); if it throws, runCatching catches the exception and you get Result.failure(exception). It is the standard way to convert exception-throwing code into a Result without writing a try/catch yourself. There is also a receiver form, value.runCatching { ... }, where 'this' inside the block is the value — convenient for chaining operations on an object that might fail.
What's the difference between getOrNull, getOrDefault, and getOrElse?
All three extract the success value but differ on failure. getOrNull() returns the value or null — simplest when null is an acceptable 'no value'. getOrDefault(fallback) returns the value or a fixed fallback you supply. getOrElse { exception -> ... } returns the value or computes a fallback from a lambda that receives the captured exception, so you can branch on what went wrong. Choose getOrNull for optional values, getOrDefault for a constant fallback, and getOrElse when the fallback depends on the error.
How do map and fold work on a Result?
map transforms the success value with a lambda and leaves a failure untouched — Result.success(21).map { it * 2 } becomes Result.success(42), while a failure stays a failure. fold collapses the Result into a single value by handling both cases at once: result.fold(onSuccess = { v -> ... }, onFailure = { e -> ... }). map is for continuing a happy-path pipeline; fold is for producing a final result (like a message or a default) no matter which branch occurred. There is also mapCatching for transforms that might themselves throw.
Should I use Result instead of throwing exceptions?
Use Result for failures that are EXPECTED and recoverable — bad user input, a missing optional file, a flaky network call — where the caller should decide what to do. It makes the possibility of failure visible in the type and avoids surprising stack unwinds. Keep throwing exceptions for truly exceptional, programmer-error situations (an illegal argument, a broken invariant) that should crash loudly during development. A common guideline: don't swallow exceptions you cannot handle, and don't use Result to hide bugs — use it to model outcomes you genuinely plan to handle.