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.

15217910de00471b472fecae440c6daf?s=128

Adam Bennett

July 14, 2020
Tweet

Transcript

  1. Adam Bennett @iateyourmic Error Handling in Kotlin The Result Monad

  2. 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
  3. Further Reading bit.ly/2Zlpzl1 adambennett.dev

  4. 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
  5. 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
  6. A Typical UseCase class FetchVehicles { suspend operator fun invoke():

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

    List<Vehicle>? = try { fetchVehicles() } catch (e: Throwable) { null } }
  8. A Typical UseCase class FetchVehicles { @Throws(NetworkException::class) suspend operator fun

    invoke(): List<Vehicle> = fetchVehicles() }
  9. A Typical UseCase class FetchVehicles { suspend operator fun invoke():

    List<Vehicle> = fetchVehicles() }
  10. 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
  11. The Result Monad • Explicitly for modelling success and failure

    states • Satisfies all of our requirements • kotlin-result • github.com/michaelbull/kotlin-result
  12. val (value, error) = Ok("Hello world!") value shouldEqual "Hello world!"

    error shouldEqual null
  13. Ok("Hello world!") .map { it.dropLast(1) } // "Hello world" .map

    { it.length } // 11 .map { it * 5 } // 55
  14. Err(Throwable("An exception")) .mapError { it.message } // "An exception" .mapError

    { it.length } // 12
  15. 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() } }
  16. 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() } }
  17. UseCases class FetchVehicles { suspend operator fun invoke(): Result<VehicleDomain, CuvvaError>

    = fetchVehicles() }
  18. In a ViewModel val vehicleList: VehicleDomain? = fetchVehicles() .get()

  19. In a ViewModel val vehicleList: VehicleDomain = fetchVehicles() .getOrElse {

    VehicleDomain.empty() }
  20. In a ViewModel viewModelScope.launch { fetchVehicles() .map { it.toPresentation() }

    .mapError { it.toErrorMessage() } .onSuccess { vehicles -> displayVehicles(vehicles) } .onFailure { msg -> displayError(msg) } }
  21. The Cuvva Codebase

  22. The Cuvva Codebase

  23. The Cuvva Codebase

  24. 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) } ) ) } } }
  25. Problem solved?

  26. Lessons learned • Not a silver bullet • For modelling

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

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

    flow • Apathy • I/O • Performance • Public APIs • Functional programming is scary
  29. 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
  30. Logging inline fun <V> runCatchingAndLog( block: () -> V ):

    Result<V, Throwable> = runCatching { block() } .onFailure { it.printStackTrace() }
  31. Typealiases Result<T, Throwable> typealias ApiResult<T> = Result<T, Throwable> Flow<Result<List<T>>, DomainError>>

    typealias DomainFlow<T> = Flow<Result<T>, DomainError>>
  32. 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
  33. Adam Bennett @iateyourmic Thank you!

  34. Addendum Exceptions in Kotlin • Kotlin has unchecked exceptions •

    Kotlin -> Java -> Bytecode • How?
  35. 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
  36. SneakyThrows fun throwSomething() { throw IOException() }

  37. SneakyThrows public final void throwSomething() { throw (Throwable)(new IOException()); }

  38. SneakyThrows public static <E extends Throwable> void sneakyThrow(Throwable e) throws

    E { throw (E) e; } private static void throwsSneakyIOException() { sneakyThrow(new IOException("sneaky")); }
  39. SneakyThrows try { return throwsSneakyIOException() } catch (IOException e) {

    e.printStackTrace() } return null
  40. SneakyThrows @Throws(IOException::class) fun throwsSneakyIOException() { throw IOException(“no longer sneaky") }

  41. SneakyThrows public final void throwsSneakyIOException() { throw (Throwable)(new IOException("no longer

    sneaky")); }
  42. SneakyThrows public final void throwsSneakyIOException() throws IOException { throw (Throwable)(new

    IOException("no longer sneaky")); }