🚀 · Returning a Result

6 min read · Updated on by

Read on to learn how to solve all the problems of exceptions by instead returning values, an introduction to the Result class, and how it makes handling unexpected errors a breeze. Finally, a quick note about how it relates to actual functional programming.

In the previous article, we showed how exceptions were basically GOTO in disguise, broke fundamental OOP principles and resulted in code that was prone to incorrect error handling, difficult to reason about and time-consuming to write correctly.

Here are the specific problems we want to solve:

  1. We want to preserve normal control flow. This means that we don’t want execution to jump a thousand light-years away from the place the error happens.
  2. We want to deal with all error states, not just the “typical” ones
  3. We want to decouple our code from the specific exceptions thrown by outside code as much as possible
  4. We want to write less code

While this may seem like a tall order, it’s actually not.

Here are some observations:

  1. Normal control flow is preserved when we exit the function by returning a value. The moment we stop throwing exceptions, we have no other way of exiting a function, so when an exception gets thrown, we need to stop re-throwing it as quickly as possible. Since exceptions are, almost without exception (ha), eventually converted to values, what we’re really saying is that we need this conversion to happen as close to the place the error happened as possible — i.e. we need to move the code that does the conversion to values as deep as we can.
  2. In 99% of the cases, all you do is transform the Throwable into a string representation that gets sent to the client (and maybe written to the log). You don't care about the type. For this reason, we can just catch Throwable (which is actually okay to do, with certain limitations) and not worry about its specific type.
  3. The remaining 1% of cases are the consequences of specific business requirements, which makes them part of the business (i.e. service) logic. If a business requirement is “When specifically error X happens, do Y”, writing service code that is tightly coupled to X being thrown is absolutely okay, as long as it’s implemented next to all the other business logic.

So, let’s switch from re-throwing exceptions to returning values. To do that, we need a datatype that denotes two situations:

  1. the code finished without issues, and the result is some value: T, and
  2. an oops happened

The first thing that comes to mind is Optional<T>, which is semantically equivalent to T?, however this is not rich enough for us. Nullables/optionals are fine for representing success, but if something goes wrong, we probably want to encode some information about what went wrong - the 'oops instance', so to speak. And nullables/optionals only have the ability to communicate "something" vs "nothing". What we want is Success(value: T) vs Failure(oops: Throwable).

So…let’s do exactly that! Let’s create a type Result<out T> and two subclasses Success<T>(val value: T): Result<T> and Failure(val error: Throwable) : Result<Nothing> and use those to wrap all of our calculations.


//sampleStart
sealed interface Result<out T>
data class Success<T>(val value: T): Result<T>
data class Failure(val error: Throwable) : Result<Nothing>

// Before
fun retrieve(id: Long): Product = try {
   productRepository.retrieveById(id)
} catch (e: ObjectNotFoundException) {
    throw ProductIdDoesntExist(e)
}

// after
fun retrieve(id: Long): Result<Product> = try {
   Success(productRepository.retrieveById(id))
} catch (e: Throwable) {
   Failure(e)
}
//sampleEnd

Hmmm…could we do better? Yes we could:


//sampleStart
fun <T> runCatching(block: () -> T): Result<T> = try { 
    Success(block()) 
} catch (e: Throwable) { 
    Failure(e) 
}

fun retrieve(id: Long): Result<Product> = runCatching {
   productRepository.retrieveById(id)
}
//sampleEnd

By introducing two simple types and a function, we were able to:

  • recover normal execution flow
  • get rid of spooky “action-at-a-distance” exception handlers
  • write code that is shorter, cleaner, and almost as simple as code where we completely ignore exceptions
  • deal with all possible errors, now and in the future
  • be explicit that this operation can fail, and use types to force calling code to deal with these failures (incidentally, this is what checked exceptions were trying, and mostly failingto do)

However, most importantly, we have shifted our mindset. We no longer view exceptions as control flow constructs, we view them as simple carriers of information — basically data classes. We don’t immediately feel the need to translate or chain them, unless there is an actual need to provide more information or react to them — for the most part, the type won’t matter.

The benefit of this approach gets multiplied when you start dealing with libraries that represent errors in different ways.

Here’s an example:


//sampleStart
// Returns an Int, throwing SomeException if it cannot do so
fun libFun1(): Int { 
   // ...  
}

// Returns an Int if it is available, or an empty optional if it is not available. Throws SomeOtherException if 
// argument is invalid 
fun libFun2(argument: Int): Optional<Int> {
   // ...
}
//sampleEnd

Given those functions, we are tasked with creating ratioOfLibFuns() which returns the Int ratio of both library functions, and deals with errors appropriately. In this specific scenario, if the Int returned by libFun2 is missing, it means that the user forgot to do something, and we want to convey this information somehow.

Here’s how we would probably implement this up until now:


//sampleStart
fun ratioOfLibFuns_oldWay(argument: Int): Int {
   try {
      val int1 = libFun1()
      val int2 = libFun2(argument).orElseThrow {
         UserRatioException("Int2 was not entered by user")
      }

      if (int2 != 0) {
         return int1/int2
      } else {
         throw GenericRatioException("Int2 is 0!")
      }

   } catch(e: SomeException) {
      throw GenericRatioException("Int2 is not available", e)
   } catch(e: SomeOtherException) {
      throw GenericRatioException("Argument to libFun2 is invalid!", e)
   }
}
//sampleEnd

Ugh, what a mess. Thankfully, using what we just discovered, we can do better:


//sampleStart
fun ratioOfLibFuns_newWay(argument: Int): Result<Int> = runCatching {
   val int1 = libFun1()
   val int2 = libFun2(argument).orElseThrow { 
       UserRatioException("Int2 was not entered by user") 
   }
   int1 / int2
}
//sampleEnd

By introducing a super simple type, we managed to make to code about 4x shorter, increase its readability and maintainability, and become explicit about the fact that it can fail.

In fact, we could go further — we could combine this approach with what we learned about strongly typed illegal states, and avoid using UserRatioException completely:


//sampleStart
sealed interface Ratio
data class RatioOf(val result: Int): Ratio
object Int2Missing: Ratio

fun ratioOfLibFuns_newWay(argument: Int): Result<Ratio> = runCatching {
    val int1 = libFun1()
    libFun2(argument)
        .map<Ratio> { int2 -> RatioOf(int1 / int2) }
        .orElse(Int2Missing)
}
//sampleEnd

Notice how much information you gain just by reading the signature of the function: it returns a Ratio instance, which recognizes a single type of business error (Int2Missing), and is wrapped in Result, which signifies that the method can fail unexpectedly. Without even looking at the implementation, you immediately know about all the things that can possibly happen in this method.

This is one of the incredibly powerful consequences of using types which have implicit meaning/behavior associated with them. You actually know this very well, but might not realize it. Think about it — Optional means a presence or absence of a value, List means multiple values, Future is a value which will exist at some point in time, Function is a value that can be producedResult is a value or a failure, and so on.

This is actually one of the core principles of actual functional programming — using these datatypes, as well as many other ones such as EitherWriterStateEval and many, many others, to represent behavior in a program, and building what we need as a composition of these fundamental, provably correct behaviors. This topic is far beyond the scope of this article, but I thought it was worth mentioning that there was a close connection with what we’re doing here with Result.

It turns out that the Result hierarchy and the runCatching method are already part of the standard library. In the next article, we’ll explore what’s included in the standard library more closely.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer