When and how to use Result in Kotlin?

TL;DR: never in business code (prefer custom sealed classes), but you can consider Result if you build a framework that must relay all kinds of errors through a different way than the exception mechanism (e.g. kotlinx.coroutines’s implementation).

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.

Also, here is a very nice article by Roman Elizarov about exceptions in Kotlin and more specifically when to use the type system instead of exceptions.

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.

Do not use runCatching nor catch Throwable in business code

Note that runCatching catches all sorts of Throwable, including JVM Errors like NoClassDefFoundError, ThreadDeath, OutOfMemoryError, or StackOverflowError. There is usually almost nothing you can do with such JVM errors (even reporting them might be impossible, for instance in case of OOME).

According to the Java documentation, the Error class “indicates serious problems that a reasonable application should not try to catch”. There are some very special cases where you might want to try to recover from error, but that is quite exceptional (pun intended).

Catch-all mechanisms like this (even catch(Exception)) are usually not recommended unless you’re implementing some kind of framework that needs to attempt to report errors in some way.

So, if we don’t catch errors (and instead let them bubble up naturally), Result isn’t part of the picture here.

Do not catch programming exceptions in business code

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. Using error(), check(), require() in the right places will make use of exceptions to detect bugs that cannot be caught by the compiler (IllegalStateException, IllegalArgumentException). It often doesn’t make sense to catch these exceptions in business code, because they appear when the programmer made a mistake and the logic of the code is broken. You should instead fix the code’s logic.

Sometimes it might still be useful to catch all exceptions (including these ones) around an area of code, and recover with a high level replacement for the whole failed operation (usually in framework code, but sometimes in business code too). This will probably be done with some try-catch(Exception), though, but Result will not be involved here because the point of such code would be to delimit with try-catch the high-level operation that can be replaced. Low-level code will not return Result because it doesn’t know whether there are higher level operations that can be replaced with something in case of programming errors, or if it should just bubble up.

Modeling business errors

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.

Leave a Comment