Result objects instead of exceptions

Exceptions have their shortcomings and APIs are more explicit without them. In Kotlin expressing the success or failure of an operation is easy and elegant with a custom class hierarchy, like Roman Elizarov suggested:

sealed class ParsedDate {
    data class Success(val date: Date) : ParsedDate()
    data class Failure(val errorOffset: Int) : ParsedDate()
}

Another option would be the use of Result type, which could include the success value or an exception.

But what to do in the case of methods that would usually return void and an exception in the case of error? Since the caller is not necessarily interested in the return value it would be easy to forget error handling:

obj.possiblyFailingOperation(x)

Maybe the root cause here is the mixture of two programming styles: imperative programming with side effects and functional style of error handling! If the obj in my example wouldn’t be mutated but a “mutated copy” would be returned by possiblyFailingOperation, then the caller would still need to do something with the result of the operation.

Here is an example of such a mixture of functional error handling and imperative side-effects:

fun possiblyFailingOperation(x: Int) Result<Unit> {
  return if (x < 0) {
    Result.failure(IllegalArgumentException()) // could easily be ignored!
  } else {
    Result.success(Unit)
  }
  // perform some side-effect
}

Pure imperative approach would look like this:

fun possiblyFailingOperation(x: Int) Result<Unit> {
  require(x > 0)
  // perform some side-effect
}

Pure functional style would like this this (assuming the function is member of Thing class):

fun possiblyFailingOperation(x: Int) : Result<Thing> {
  return if (x < 0) {
    Result.failure(IllegalArgumentException())
  } else {
    Result.success(this.copy(x = x))
  }
}

So, does error handling without exceptions make sense with imperative OOP code at all? Wouldn’t it be better to decide for one of the pure approaches?

A mixed approach could be to use result objects only for methods returning a value and exceptions only for mutation methods, but then there would be two approaches for the same thing in a single code base and even in the same class!

In the case of using exception as a return value (and not throwing them) would it make sense to create a new instance in every case with its own (costly) stacktrace or would it be sufficient to have singleton?

3 Likes

I think this is one of reasons why exceptions were invented in the first place. Initially, we often returned “status” from many functions and the caller should verify whether it was success or not. But it turned out people very often miss errors. Exceptions are a more “aggressive” way of signalling errors.

I’m also interested, how people who prefer “either/result” over exceptions handle this problem :slight_smile:

4 Likes

Maybe it was not clear enough in my examples, but I think the “trick” to force the caller to handle the result with the Either/Result approach, is to avoid side-effects. If you don’t have side-effects the operation is only useful if you get the mutated copy.

First let’s have a look at the imperative, exception-based approach:

class MutableThing(value: Int) {
  value: Int = value
    private set

  fun operate(x: Int) {
    require(x > 0)
    value = value + x
  }
}

If a caller would call operate he can not forget to handle the error case:

val thing = MutableThing(1)
thing.operate(-1) // bang!

The same with a result object wouldn’t force the caller to handle the in principle uninteresting result:

class MutableThing(value: Int) {
  value: Int = value
    private set

  fun operate(x: Int) : Result<Unit> {
    if (x <= 0) {
      return Result.failure(IllegalArgumentException())
    } 
    value = value + x
    return Result.success(Unit)
  }
}

Since the caller doesn’t need the result, he can easily ignore it:

val thing = MutableThing(1)
thing.operate(-1) // result ignored, error swallowed!

The solution would be to have an immutable class, because then only the result of an operation on such an object would be useful, since the original object would stay the same:

class ImmutableThing(val value: Int) {
  fun operate(x: Int): Result<ImmutableThing> {
    if (x <= 0) {
      return Result.failure(IllegalArgumentException())
    } 
    val updatedCopy = Result(value + x)
    return Result.success(updatedCopy)
}

Now the caller is forced to to something with the result.

val thing = MutableThing(1)
thing.operate(-1)
  .onSuccess { ... }
  .onFailure { ... }

That’s why I’m questioning whether one should decide for a pure functional approach with result objects and immutable objects or for a pure imperative approach with mutable objects and exceptions.

I’ve recently changed my style to use immutability with Result instead of throwing exceptions. Throwing exceptions was certainly “easier” because it required less code and less thinking, but I feel the advantages of maintenance and clarity will outweigh the costs.

1 Like

I would definitely be against doing something like.

if (x <= 0) {
    return Result.failure(IllegalArgumentException())
}

IllegalArgumentException and etc. are system failures not a business logic check. So it better be written like and should not be caught/handled.

require(x <= 0) {"x must never be equal or less than zero "}

If your x <= 0 check is related to business logic then you should have your own custom exception or error type.

I’d say that the form in which errors are communicated is not limited to certain cases (e.g. business logic or not). Rust, for example, doesn’t have exceptions at all, so another mechanism must be used for all error cases in Rust. But I agree with you, that it is at least uncommon to handle errors in that way in Java and Kotlin.