skip to content

When should you choose runCatching over a plain try/catch, and what are the trade-offs?

level: middleimportance: should knowfreq 38%

answer

  1. runCatching = error as a value, chainable (map/fold/recover)
  2. try/catch = targeted types + finally cleanup
  3. runCatching catches ALL Throwable (too broad)
  4. Loops collecting outcomes → runCatching
  5. Coroutine catch-all → try/catch rethrowing cancellation

basics

~20 s

Use runCatching when you want the outcome as a value you can pass around, transform with map/fold, or chain. Use try/catch when you only need to react to specific exceptions in place. runCatching catches everything, which is sometimes too broad.

solid answer

~40 s

`runCatching` shines when the error needs to become a *value*: when you want to chain (`map`, `recover`, `fold`), collect outcomes across a loop, or defer handling to a caller. `try/catch` is better when you need to handle *specific* exception types differently right where they occur, or do cleanup with `finally`. Trade-offs: `runCatching` catches all `Throwable` (including `CancellationException` and arguably `OutOfMemoryError`), so it is bluntly broad — `try/catch` lets you target `catch (e: IOException)` and let everything else propagate. `runCatching` reads as a clean expression and is `inline` (no lambda allocation), but it discards the precision of typed catches. Rule of thumb: `runCatching` for value-oriented, deferred, or chained handling at boundaries; `try/catch` for targeted, immediate, type-specific handling and resource cleanup.

code

kotlin · 12 lines
kotlin
// runCatching: collect & partition outcomes
val (ok, failed) = ids.map { id -> id to runCatching { load(id) } }
    .partition { it.second.isSuccess }

// try/catch: type-specific handling + cleanup
fun read(f: File): String = try {
    f.readText()
} catch (e: FileNotFoundException) {
    ""
} finally {
    metrics.increment("reads")
}

go deeper

for a junior

Knows runCatching gives a Result while try/catch reacts in place.

for a middle

Picks the right tool by need (chain/collect vs typed-handling/cleanup) and knows runCatching's breadth.

for a senior

Reasons about catching Error/CancellationException, combining runCatching with fold/recover, and readability trade-offs.

for a principal

Establishes when each is idiomatic in the codebase and how it interacts with cancellation and observability.

## Two tools, different jobs Both convert thrown exceptions into something manageable, but at different granularities. ### try/catch — targeted, imperative ```kotlin val data = try { repo.load(id) } catch (e: NotFoundException) { return null // specific reaction, in place } catch (e: IOException) { throw RetryableError(e) // translate one type } finally { lock.unlock() // cleanup regardless } ``` - You can **match specific types** and let others propagate. - `finally` gives deterministic cleanup. - It is a statement/expression evaluated right here. ### runCatching — value-oriented, functional ```kotlin val data: Result<Data> = runCatching { repo.load(id) } val label = data .map { it.title } .recover { "unknown" } .getOrThrow() ``` - Produces a `Result` you can **store, return, pass, and transform** (`map`, `mapCatching`, `recover`, `fold`, `onSuccess`/`onFailure`). - Great for **collecting** outcomes: `items.map { runCatching { process(it) } }` then partition successes/failures. - It is `inline`, so no allocation for the block. ## The breadth problem `runCatching` catches **all `Throwable`**. That is convenient but blunt: - It captures `CancellationException` in coroutines (breaks cancellation — see the cancellation question). - It captures `Error`s like `OutOfMemoryError`/`StackOverflowError`, which you usually should not handle. - It cannot, by itself, treat different exception types differently — you have to inspect `exceptionOrNull()` afterward and branch with `is`. `try/catch` avoids this by letting you name exactly the types you want. ## Decision guide | Want… | Prefer | |-------|--------| | Outcome as a passable value / chain it | `runCatching` | | Collect many outcomes in a loop | `runCatching` | | Handle specific exception types differently | `try/catch` | | Deterministic cleanup (`finally`) | `try/catch` | | Inside a coroutine, catch-all | `try/catch` rethrowing `CancellationException`, or a cancellation-safe wrapper | ## Combine them It's common to use `runCatching` for the happy/error split, then `fold` or `getOrElse` to branch on `exceptionOrNull()` types — but if the branching is non-trivial, a typed `try/catch` is clearer.

  • How do you do type-specific handling with runCatching?
    Inspect exceptionOrNull() and branch with is checks, e.g. fold(onFailure = { when (it) { is IOException -> ...; else -> ... } }). For more than one or two types, a typed try/catch is usually clearer.
  • Does runCatching support a finally-like cleanup?
    No built-in finally. Use use {} for closeables inside the block, or a try/finally; runCatching only converts the outcome to a Result.

saying these in an interview costs you the question

  • Saying runCatching is always cleaner so try/catch is obsolete
  • Ignoring that runCatching catches Error and CancellationException
  • Using runCatching where one specific exception type needs handling
  • Forgetting there is no finally with runCatching
  • Wrapping a whole function in runCatching just to avoid declaring throws

context