When should you choose runCatching over a plain try/catch, and what are the trade-offs?
answer
- runCatching = error as a value, chainable (map/fold/recover)
- try/catch = targeted types + finally cleanup
- runCatching catches ALL Throwable (too broad)
- Loops collecting outcomes → runCatching
- Coroutine catch-all → try/catch rethrowing cancellation
basics
~20 sUse 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// 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
Knows runCatching gives a Result while try/catch reacts in place.
Picks the right tool by need (chain/collect vs typed-handling/cleanup) and knows runCatching's breadth.
Reasons about catching Error/CancellationException, combining runCatching with fold/recover, and readability trade-offs.
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