How to Handle Kotlin Exceptions When a Method Throws: A Complete Guide to Result and runCatching

Use the runCatching helper to wrap exception-throwing code in a Result type, then chain methods like getOrElse, getOrThrow, or fold to handle success and failure cases without nested try/catch blocks.

Kotlin provides a type-safe, functional approach to handle kotlin exceptions through the standard library's Result type and runCatching utility. Unlike Java's checked exceptions or basic try/catch blocks, these tools allow you to capture any Throwable in a discriminated union that preserves both success values and failure states. According to the JetBrains/kotlin source code, the core implementation resides in libraries/stdlib/src/kotlin/util/Result.kt, where the runCatching inline function (lines 41-61) automatically wraps execution outcomes while catching all throwables.

Understanding the Result Type and runCatching

The Result<T> class is a value class defined in libraries/stdlib/src/kotlin/util/Result.kt that represents either a successful value of type T or a failure containing a Throwable. This discriminated union eliminates the need for null checks or separate error flags.

The runCatching function provides the primary entry point to handle kotlin exceptions functionally:

@InlineOnly
@SinceKotlin("1.3")
public inline fun <R> runCatching(block: () -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

This inline function catches any Throwable, including unchecked exceptions and errors, wrapping them in a Result.failure instance. Because it is inline, the lambda body compiles directly into the call site with zero overhead from function objects.

Core Methods for Extracting Values and Handling Failures

Once you have a Result instance, the standard library provides several extension functions to handle kotlin exceptions without explicit try/catch blocks. These are implemented in libraries/stdlib/src/kotlin/util/Result.kt (lines 71-84).

getOrElse and getOrThrow

Use getOrElse to provide a fallback value when an exception occurs:

val content = runCatching { File("config.json").readText() }
    .getOrElse { throwable ->
        logger.warn("Could not read config: ${throwable.message}")
        "{}"  // Return default empty JSON
    }

Use getOrThrow when you want to re-throw the captured exception:

val criticalValue = runCatching { fetchMandatoryData() }
    .getOrThrow()  // Throws the original Throwable if failure

fold for Explicit Branching

The fold method combines success and failure handling into a single expression:

runCatching { processPayment() }.fold(
    onSuccess = { transactionId -> println("Success: $transactionId") },
    onFailure = { error -> println("Failed: ${error.message}") }
)

getOrNull and exceptionOrNull

For cases where you only care about the value and want to ignore errors:

val maybeValue: Int? = runCatching { parseInt(input) }.getOrNull()
val error: Throwable? = runCatching { riskyOperation() }.exceptionOrNull()

Chaining Operations with map and flatMap

Functional composition allows you to handle kotlin exceptions across multiple dependent operations without nested try/catch blocks.

Transforming Success Values with map

val finalResult = runCatching { fetchUserId() }
    .map { id -> fetchUserProfile(id) }  // Only runs if fetchUserId succeeded
    .map { profile -> profile.email }

Chaining Multiple Result-Producing Operations with flatMap

When subsequent operations might also throw, use flatMap to avoid nested Result types:

val composedResult = runCatching { step1() }
    .flatMap { intermediate ->
        runCatching { step2(intermediate) }  // Returns Result, flattened automatically
    }
    .flatMap { runCatching { step3(it) } }

Comparing runCatching with Classic Try/Catch

While runCatching provides elegant functional handling, classic try / catch blocks remain appropriate in specific scenarios.

Approach Best For Syntax Example
runCatching Functional pipelines, returning results from functions, composing operations runCatching { foo() }.map { bar(it) }.getOrElse { default }
try/catch Resource management (try-with-resources), performance-critical code, specific exception types try { file.use { it.readText() } } catch (e: IOException) { ... }

According to the implementation in libraries/stdlib/src/kotlin/util/Result.kt, runCatching catches any Throwable, which includes Error subclasses like OutOfMemoryError. This differs from Java's checked exception model and provides uniform handling regardless of exception type.

Practical Examples for Common Scenarios

Handling File I/O Exceptions

import java.io.File

fun readConfig(path: String): Result<String> = runCatching {
    File(path).readText()  // May throw IOException
}

// Caller decides how to react
val content = readConfig("app.conf")
    .getOrElse { e ->
        logger.warn("Could not read config: ${e.message}")
        "{}"  // default empty JSON
    }
    .let { parseJson(it) }

Network Request Error Handling

import java.net.URL

fun downloadData(url: String): Result<ByteArray> = runCatching {
    URL(url).readBytes()  // May throw IOException
}

val data = downloadData("https://api.example.com/data")
    .getOrElse { 
        logger.error("Download failed", it)
        ByteArray(0)  // empty payload as fallback
    }

Composing Multiple Risky Operations

fun calculateRisky(): Double = runCatching { fetchNumber() }
    .map { it * 2 }
    .flatMap { doubled ->
        runCatching { divideByInput(doubled) }
    }
    .getOrElse { 0.0 }

Summary

  • Use runCatching to wrap exception-throwing code in a Result type without explicit try/catch blocks, as implemented in libraries/stdlib/src/kotlin/util/Result.kt.
  • Leverage getOrElse, getOrThrow, and fold to extract values, provide fallbacks, or branch on success/failure with functional syntax.
  • Chain operations using map and flatMap to compose multiple exception-prone steps into clean, readable pipelines.
  • Reserve classic try/catch for resource management (try-with-resources) or when you need to catch specific exception types without the overhead of Result allocation.
  • Remember that runCatching catches any Throwable, providing uniform handling for checked, unchecked, and error conditions alike.

Frequently Asked Questions

What is the difference between runCatching and a try/catch block?

runCatching is an inline function that automatically wraps the outcome of a code block in a Result type, catching any Throwable and storing it as a failure. This allows functional chaining with map, flatMap, and fold. A classic try/catch block requires manual exception handling and does not return a composable result object, making runCatching preferable for functional pipelines while try/catch remains better for resource management with finally blocks.

How do I extract a value from a Result or provide a default if it failed?

Use the getOrElse extension function, which accepts a lambda that receives the Throwable and returns a fallback value. For example: runCatching { riskyCall() }.getOrElse { e -> defaultValue }. If you prefer to re-throw the exception instead, use getOrThrow(), which returns the success value or throws the stored Throwable if the result represents a failure.

Can I chain multiple operations that might throw exceptions?

Yes, use map to transform success values and flatMap to chain operations that themselves return Result types. For instance: runCatching { step1() }.map { step2(it) }.flatMap { runCatching { step3(it) } }. This creates a composable pipeline where any failure short-circuits subsequent operations, and you handle the final outcome with fold, getOrElse, or getOrThrow at the end of the chain.

When should I avoid runCatching and use traditional try/catch instead?

Avoid runCatching in performance-critical sections where allocating a Result object introduces unacceptable overhead, or when you need fine-grained control over specific exception types without catching all Throwable subclasses. Additionally, use traditional try/finally or use blocks when managing resources that require guaranteed cleanup (like file streams or database connections), as runCatching does not inherently support the equivalent of a finally block for resource management.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →