Arrow Try is dead, long live Kotlin Result

Juan Rada
3 min readJan 29, 2020

--

A few months ago Arrow Try was deprecated and I was needing an alternative data structure for declarative error handling. For the ones not familiar with this data structure type, it is a type able to store a particular function execution result which can be a success or a failure. Pretty similar to Java Optional but instead of value or empty it contains value or exception.

val fileContent: Either<String> = Try { 
return readFileContent(file)
}
result.fold(
{"Error reading file ${it.message}"},
{"Successfully read ${it.length}" }
)

Luckily for me, Kotlin provides Result since version 1.3 which has a similar functionality to Arrow Try, it is an inline class so the overhead of wrapper data structure is zero (in many cases) and the standard library provides extensions functions to integrate it into your business logic. This blog post contains an example and description of how to use it and its utility functions.

Getting started
First, as its names indicate Result represents an execution result, to encapsulate your function execution, to instantiate it runCatching function can be used which is pretty much a declarative version of classic imperative try and catch.

val execResult: Result<Instant> = runCatching {
Instant.parse(dateString)
}

Which is equivalent to (Imperative version)

try {
val date = Instant.parse(date);
return Result.success(date)
}
catch (exception: Exception) {
return Result.failure(exception)
}

That it is all, simple and concise. Now you have your execution encapsulated into a wrapper. But the story does not end here, altogether with Result type, Kotlin standard library provides a set of functions to easily integrate it into your fail probe system, these functions are also inline functions which in resume means that its code will be copied into your function saving you the function stack call.

Map
The map function allows changing the underlying value (if the execution was successful), let’s say we need to parse a string that contains and Instant but an OffsetDateTime is expected.

runCatching { Instant.parse(date) }
.map { OffsetDateTime.ofInstant(it, ZoneOffset.UTC) }

Fold
The fold function allows mapping Result value or exception. It’s really useful when, for example, a default value needs to be used in case of error and a transformation is needed over the value in case of success.

runCatching { Instant.parse(date) }.fold (
{ OffsetDateTime.ofInstant(it, ZoneOffset.UTC) },
{
OffsetDateTime.now() }
)

GetOrNull/GetOrDefault
These functions can be considered specification of fold function and allow either return null or default in case of execution failure.

val date:Instant = runCatching { Instant.parse(date) }.getOrDefault(Instant.now())val dateOrNull: Instant? =  runCatching {
Instant.parse(date)
}.getOrNull()

Error Handling and recovery
Some other scenarios may require complex error handling strategies which may include recovering or error reporting. For example, let’s imagine we want to store a record in a remote service but if the operation fails we want to do it locally.

runCatching {
saveRecordRemotly(myRecord)
}.recover {
logger.error({ "saving record remotly fail"}, it)
saveLocally(myRecord")
}

Note that in this case if saveLocally throws an exception it will be rethrown.

Error Error handling
No, it is not a typo, Kotlin guys also think in the scenarios when your recover function may throw an error and you want to encapsulate that exception too, to avoid unexcepted unhandled exceptions.

val exceptionResult:Result<String> = runCatching { 
h
ttpClient.get<String>(mainServer)
}.recoverCatching {
logger.error({ "Main server request fail"}, it)
httpClient.get<String>(alternativeServer)
}

In this case, even if the alternative server request also fails, the value will be encapsulated into results and exception will not be throw.

Also is possible to use a fail probe mapping function in the same way. Imagine that we have master details records and we want the detail record of each master but we want to capture errors in both master and detail record retrieval.

val exceptionResult:Result<String> = runCatching { 
h
ttpClient.get<SimpleRecord>(mainServer)
}.mapCatching {
httpClient.get<RecordWithDetails>(alternativeServer)
}

That is all, hope it was clear. I strongly recommend using runCatching/Result to limit exception propagation and create failover probe functions. Also, it is elegant, efficient and clean.

--

--