Slide 1

Slide 1 text

Adam Bennett @iateyourmic Error Handling in Kotlin The Result Monad

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Further Reading bit.ly/2Zlpzl1 adambennett.dev

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

A Typical UseCase class FetchVehicles { suspend operator fun invoke(): List? = try { fetchVehicles() } catch (e: Throwable) { null } }

Slide 8

Slide 8 text

A Typical UseCase class FetchVehicles { @Throws(NetworkException::class) suspend operator fun invoke(): List = fetchVehicles() }

Slide 9

Slide 9 text

A Typical UseCase class FetchVehicles { suspend operator fun invoke(): List = fetchVehicles() }

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

The Result Monad • Explicitly for modelling success and failure states • Satisfies all of our requirements • kotlin-result • github.com/michaelbull/kotlin-result

Slide 12

Slide 12 text

val (value, error) = Ok("Hello world!") value shouldEqual "Hello world!" error shouldEqual null

Slide 13

Slide 13 text

Ok("Hello world!") .map { it.dropLast(1) } // "Hello world" .map { it.length } // 11 .map { it * 5 } // 55

Slide 14

Slide 14 text

Err(Throwable("An exception")) .mapError { it.message } // "An exception" .mapError { it.length } // 12

Slide 15

Slide 15 text

The Data Layer interface VehicleApi { @GET("/garage") suspend fun getVehicles(): List } class VehicleService(private val api: VehicleApi) { suspend fun getVehicles(): Result>, Throwable> = runCatching { api.getVehicles() } }

Slide 16

Slide 16 text

The Data Layer suspend fun getPolicies(): Result = authService.getToken() .mapError { DomainError.AuthException() } .andThen { token -> vehicleService.getVehicles(token) .andThen { vehicles -> policyService.getPolicies(token, vehicles) } .map { policies -> policies.toDomain() } .mapError { throwable -> throwable.toDomainError() } }

Slide 17

Slide 17 text

UseCases class FetchVehicles { suspend operator fun invoke(): Result = fetchVehicles() }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

In a ViewModel viewModelScope.launch { fetchVehicles() .map { it.toPresentation() } .mapError { it.toErrorMessage() } .onSuccess { vehicles -> displayVehicles(vehicles) } .onFailure { msg -> displayError(msg) } }

Slide 21

Slide 21 text

The Cuvva Codebase

Slide 22

Slide 22 text

The Cuvva Codebase

Slide 23

Slide 23 text

The Cuvva Codebase

Slide 24

Slide 24 text

val transformer: FlowTransformer { upstream -> upstream.flatMapLatest { flow { emit(Update.Loading) emit( getPolicies() .fold( success = { policies -> Update.Success(policies) }, failure = { error -> Update.Error(it) } ) ) } } }

Slide 25

Slide 25 text

Problem solved?

Slide 26

Slide 26 text

Lessons learned • Not a silver bullet • For modelling expected control flow • Solution is not to wrap everything! • Some types of exception must remain exceptional

Slide 27

Slide 27 text

Lessons learned • Consume within Flows • Avoid using Flows with Result as a return type • …at least not if you combine them regularly

Slide 28

Slide 28 text

Reasons Against • Fail fast • Local exceptions for control flow • Apathy • I/O • Performance • Public APIs • Functional programming is scary

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Logging inline fun runCatchingAndLog( block: () -> V ): Result = runCatching { block() } .onFailure { it.printStackTrace() }

Slide 31

Slide 31 text

Typealiases Result typealias ApiResult = Result Flow>, DomainError>> typealias DomainFlow = Flow, DomainError>>

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Adam Bennett @iateyourmic Thank you!

Slide 34

Slide 34 text

Addendum Exceptions in Kotlin • Kotlin has unchecked exceptions • Kotlin -> Java -> Bytecode • How?

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

SneakyThrows fun throwSomething() { throw IOException() }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

SneakyThrows try { return throwsSneakyIOException() } catch (IOException e) { e.printStackTrace() } return null

Slide 40

Slide 40 text

SneakyThrows @Throws(IOException::class) fun throwsSneakyIOException() { throw IOException(“no longer sneaky") }

Slide 41

Slide 41 text

SneakyThrows public final void throwsSneakyIOException() { throw (Throwable)(new IOException("no longer sneaky")); }

Slide 42

Slide 42 text

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