Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Error Handling & The Result Monad

Kotlin Error Handling & The Result Monad

Kotlin doesn’t force you to handle functions that might throw exceptions, so how can we best ensure that our code is safe to execute whilst handling errors in a reasonable way? In this talk, we’ll look at some strategies that the team at Cuvva evaluated and discuss in-depth the solution they chose: the Result Monad. We’ll talk about how it works, discuss how utilising this strategy can help you, and how we adopted these to great effect at Cuvva.

Adam Bennett

July 14, 2020
Tweet

More Decks by Adam Bennett

Other Decks in Programming

Transcript

  1. Introduction • Why errors in Kotlin are a tricky problem

    • What were the issues we were trying to solve • Why we chose the Result Monad • How we integrated it • Lessons learned
  2. Exceptions • Exceptions can be a valid part of control

    flow • Many places where exceptions are expected • Network requests • Loading a file that the user has selected • Domain logic - selecting an out of stock item • Model them where possible rather than try/catch
  3. Kotlin Exceptions • Kotlin has unchecked exceptions • Callers must

    check method signature • Declared using @Throws(Exception::class) • … But not required • We can’t rely on consumers to handle appropriately
  4. A Typical UseCase class FetchVehicles { suspend operator fun invoke():

    List<Vehicle> = try { fetchVehicles() } catch (e: Throwable) { emptyList() } }
  5. A Typical UseCase class FetchVehicles { suspend operator fun invoke():

    List<Vehicle>? = try { fetchVehicles() } catch (e: Throwable) { null } }
  6. Our Dream API • Wrap functions that we expect to

    fail • Model both success and failure more succinctly • Map success and error types across data/domain/presentation boundaries • Chain these operations • Short-circuit on failure • Make it absolutely clear to consumer, but allow flexibility • Integrate well with Flow, Coroutines
  7. The Result Monad • Explicitly for modelling success and failure

    states • Satisfies all of our requirements • kotlin-result • github.com/michaelbull/kotlin-result
  8. Ok("Hello world!") .map { it.dropLast(1) } // "Hello world" .map

    { it.length } // 11 .map { it * 5 } // 55
  9. The Data Layer interface VehicleApi { @GET("/garage") suspend fun getVehicles():

    List<Vehicle> } class VehicleService(private val api: VehicleApi) { suspend fun getVehicles(): Result<List<Vehicle>>, Throwable> = runCatching { api.getVehicles() } }
  10. The Data Layer suspend fun getPolicies(): Result<PolicyDomain, DomainError> = authService.getToken()

    .mapError { DomainError.AuthException() } .andThen { token -> vehicleService.getVehicles(token) .andThen { vehicles -> policyService.getPolicies(token, vehicles) } .map { policies -> policies.toDomain() } .mapError { throwable -> throwable.toDomainError() } }
  11. In a ViewModel viewModelScope.launch { fetchVehicles() .map { it.toPresentation() }

    .mapError { it.toErrorMessage() } .onSuccess { vehicles -> displayVehicles(vehicles) } .onFailure { msg -> displayError(msg) } }
  12. val transformer: FlowTransformer<Input, Update> { upstream -> upstream.flatMapLatest { flow

    { emit(Update.Loading) emit( getPolicies() .fold( success = { policies -> Update.Success(policies) }, failure = { error -> Update.Error(it) } ) ) } } }
  13. Lessons learned • Not a silver bullet • For modelling

    expected control flow • Solution is not to wrap everything! • Some types of exception must remain exceptional
  14. Lessons learned • Consume within Flows • Avoid using Flows

    with Result as a return type • …at least not if you combine them regularly
  15. Reasons Against • Fail fast • Local exceptions for control

    flow • Apathy • I/O • Performance • Public APIs • Functional programming is scary
  16. Lessons learned • Don’t forget logging! • Too easy to

    swallow exceptions • Failures with no logs are not fun to debug • Don’t wrap exceptions inside domain error types
  17. Logging inline fun <V> runCatchingAndLog( block: () -> V ):

    Result<V, Throwable> = runCatching { block() } .onFailure { it.printStackTrace() }
  18. To Sum Up • Fantastic tool for modelling control flow

    • Not a catch-all • Still have to consume 3rd-party APIs diligently • Result is not always appropriate
  19. Exceptions in Kotlin • Checked exceptions are a Java feature,

    not JVM! • In bytecode, you can throw without restrictions • Kotlin compiler hides exceptions behind Throwable • Implemented by both checked and unchecked exceptions
  20. SneakyThrows public static <E extends Throwable> void sneakyThrow(Throwable e) throws

    E { throw (E) e; } private static void throwsSneakyIOException() { sneakyThrow(new IOException("sneaky")); }