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

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)

Pro Tips

📋 Quick Reference — Result API

NeedCall
Capture outcomerunCatching { ... }
Value or nullgetOrNull()
Value or constantgetOrDefault(x)
Value or computedgetOrElse { e -> ... }
Transform successmap { ... }
Handle bothfold(onSuccess, onFailure)

🎉 Lesson Complete!

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

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.