skip to content

What does Result.recover (and recoverCatching) do, and how is it the dual of map?

level: seniorimportance: should knowfreq 40%

answer

  1. recover = dual of map, acts on the failure branch
  2. recover returns Result; getOrElse returns bare R
  3. recoverCatching catches throws from the recovery lambda
  4. Success passes through recover unchanged
  5. Chain recoverCatching for layered fallbacks

basics

~20 s

recover turns a failed Result back into a success by running a lambda on the exception to produce a fallback value. Successes pass through untouched. recoverCatching is the same but catches exceptions thrown inside that recovery lambda.

solid answer

~40 s

recover(transform: (Throwable) -> R): Result<R> is the failure-side mirror of map. On a failure it calls transform with the captured Throwable and re-wraps the returned value as a success; on a success it passes the value through unchanged. So map transforms the success branch, recover transforms the failure branch — both keep the result inside Result<R>. Unlike getOrElse, which exits the pipeline returning a bare R, recover stays in Result<R> so you can keep chaining (e.g. .recover { fallback() }.map { ... }). recoverCatching catches exceptions thrown by the recovery lambda and produces a new failure, exactly as mapCatching does for map. Use recover for fallback/degradation logic (cache miss -> default, primary source down -> secondary) when you want continued composition; use getOrElse when you're done and want the raw value.

code

kotlin · 5 lines
kotlin
fun config(): Result<Settings> =
    loadRemoteConfig()                       // Result<Settings>
        .recoverCatching { parseLocalFile() } // failure -> try local (may throw)
        .recover { Settings.DEFAULTS }        // still failing -> hard default
// recover never fails here (default is total), so config() is always success

go deeper

for a junior

May know recover provides a fallback but is unlikely to articulate it as a Result-returning failure-branch transform.

for a middle

States recover converts failures to successes and that recoverCatching guards the lambda.

for a senior

Explains recover as the dual of map, distinguishes recover (Result) from getOrElse (bare R), and composes layered recoverCatching fallbacks.

for a principal

Designs degradation/fallback strategies, reasons about which failures are recoverable, and handles cancellation correctly in coroutine pipelines.

## recover — transform the failure into a success value ```kotlin inline fun <R, T : R> Result<T>.recover(transform: (Throwable) -> R): Result<R> ``` - On a **failure**: calls `transform(exception)` and wraps the returned value as `Result.success`. - On a **success**: returns the value unchanged (re-typed to `Result<R>` since `T : R`). It is the precise **dual of `map`**: | Operator | Acts on | Leaves untouched | |----------|---------|------------------| | `map` | success value | failure | | `recover`| failure exception | success | ```kotlin val r: Result<Int> = runCatching { primarySource() } .recover { e -> cachedValueOrDefault() } // failure -> success(fallback) ``` ## recover vs getOrElse — the key distinction Both take `(Throwable) -> R` on the failure branch, but: - **`recover`** returns `Result<R>` — you stay in the pipeline and can keep chaining (`.map`, `.onSuccess`, another `.recover`). - **`getOrElse`** returns a bare `R` — it *terminates* the pipeline. ```kotlin val value: Int = runCatching { primary() } .recover { secondary() } // still Result<Int> .map { it + 1 } // can keep transforming .getOrElse { -1 } // now exit with a plain Int ``` ## recoverCatching — when recovery itself can throw ```kotlin inline fun <R, T : R> Result<T>.recoverCatching(transform: (Throwable) -> R): Result<R> ``` If the recovery `transform` throws, `recoverCatching` captures it as a new failure (the original failure is replaced by the new one), exactly as `mapCatching` does for `map`. Plain `recover` lets a throw from the recovery lambda escape. ## Composition pattern: layered fallbacks ```kotlin val data = loadFromNetwork() .recoverCatching { loadFromDisk() } // network failed -> try disk .recoverCatching { loadBundledDefault() } // disk failed -> bundled .getOrThrow() // give up and surface the last error ``` Each `recoverCatching` only runs if the prior stage is still a failure; a success short-circuits the rest. Note the same `Throwable`-catching caveat as `mapCatching`: in coroutines, re-throw `CancellationException` from inside the recovery lambda.

  • How does recover differ from getOrElse when both take (Throwable) -> R?
    recover returns Result<R> so you can keep chaining; getOrElse returns a plain R and ends the pipeline.
  • What happens to the original failure if the recover lambda also throws?
    With plain recover the new exception escapes the call. With recoverCatching it is captured as a new failure, replacing the original one.

map repaints the good parts; recover is the repair shop that fixes broken parts so they re-enter the line — and recoverCatching is a repair shop with its own reject bin.

saying these in an interview costs you the question

  • Saying recover runs on a success (it only acts on failures)
  • Confusing recover with getOrElse (recover stays in Result)
  • Not knowing recoverCatching guards the recovery lambda
  • Claiming recover keeps the result a failure
  • Forgetting the CancellationException hazard in recoverCatching

context