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?