I love the idea of Result. I love having encapsulated try/catch.
But I’m a little confused about how and when to use Result.
I currently use it like this:
My adapters and services return a Result. Failures and stacktraces are logged but do nothing else
runCatching{
.... // do something cool
}.onFailure {
logger.error("Something bad happened ", it)
}
My Resource classes fold and handle the Result
return service.method().fold(
onFailure = {
Response.serverError().entity("Oops").build()
},
onSuccess = {
Response.ok().entity(doSomethingWith(it)).build()
}
)
Is this really the correct way to use a Result? Or is there a more idiomatic way to code in Kotlin?
CodePudding user response:
First, there is actually a list of use cases for the motivation of the initial introduction of Result, if you find it interesting. Also in the same document:
The Result class is designed to capture generic failures of Kotlin functions for their latter processing and should be used in general-purpose API like futures, etc, that deal with invocation of Kotlin code blocks and must be able to represent both a successful and a failed result of execution. The Result class is not designed to represent domain-specific error conditions.
Most of what follows is my personal opinion. It's built from facts, but is still just an opinion, so take it with a grain of salt.
Note that runCatching catches all sorts of Throwable, including JVM errors like OutOfMemoryError, NoClassDefFoundError or StackOverflowError. IMO it is bad practice to use catch-all mechanisms like this unless you're implementing some kind of framework that needs to report errors in a different way (for instance Kotlinx Coroutines).
Apart from JVM errors, I believe exceptions due to programming errors shouldn't really be handled in a way that bloats the business code either (by this, I mean that result types are not very appropriate in this case). Using error(), check(), require() in the right places will make use of exceptions that often don't make sense to catch in business code (IllegalStateException, IllegalArgumentException). Again, maybe it could be relevant to catch them in framework code.
If you really need to express "I want to catch any exception for this piece of code in case there is a bug so I can still do that other thing", then it would make sense to use a try-catch(e: Exception) for this, but it shouldn't catch Throwable, so still no runCatching here.
That leaves business errors for result-like types. By business errors, I mean things like missing entities, unknown values from external systems, bad user input, etc. However, I usually find better ways to model them than using kotlin.Result (it's not meant for this, as the design document stipulates). Modelling the absence of value is usually easy enough with a nullable type fun find(username: String): User?. Modelling a set of outcomes can be done with a custom sealed class that cover different cases, like a result type but with specific error subtypes (and more interesting business data about the error than just Throwable).
So in short, in the end, I never use kotlin.Result myself in business code (I could consider it for generic framework code that needs to report all errors).
My adapters and services return a Result. Failures and stacktraces are logged but do nothing else
A side note on that. As you can see, you're logging errors in the service itself, but it's unclear from the service consumer's perspective. The consumer receives a Result, so who's reponsible with dealing with the error here? If it's a recoverable error then it may or may not be appropriate to log it as an error, and maybe it's better as a warning or not at all. Maybe the consumer knows better the severity of the problem than the service. Also, the service makes no difference between JVM errors, programming errors (IAE, ISE, etc.), and business errors in the way it logs them.
