Better Error Handling In Kotlin With Either Type
Here is a nice experiment (borrowed from FP languages). Define the Either type:
1
2
3
4
sealed class Either<out T> {
data class Error(val message: String?, val e: Exception) : Either<Nothing>()
data class Success<T>(val value: T) : Either<T>()
}
Any computation that has the potential of erroring out can return Either
type instead. This makes sure that when the result of the computation is used in a when
block (when
does exhaustive smart cast checks only when used as an expression), the developer is forced to consider both the cases.
1
2
3
4
5
6
7
fun doSomething(): String? {
val result: Either<String> = echo("Hello World")
return when(result) {
is Either.Success -> result.value
is Either.Error -> result.message
}
}
In the above example, the echo
function either returns an Either.Success<String>
or an Either.Error<Nothing>
. Depending upon the type of result, we can return an appropriate value from doSomething
. We can also put any recovery mechanism here. The use when
as an expression makes the compiler force us to consider all the possible values of result
.
Let us make a special execution block named attempt<T>
. attempt
always returns an Either<T>
value. attempt
optionally takes a variable number of Either<V>
values as an argument and checks whether any of them is of Either.Error
type. If yes, it throws the exception back; if no, it calls the last argument to it, a lambda, with the given parameters. Multiple attempt
blocks can be nested and finally we only need to handle the exception once in the end (and we are required by the compiler to handle it). Doing this with try/catch blocks would have required using an all encompassing try/catch block at the top level. Here is the code for the attempt
block:
1
2
3
4
5
6
7
8
inline fun <T> attempt(vararg args: Either<Any>, body: (args: List<Any>) -> Either<T>): Either<T> {
args.filterIsInstance<Either.Error>().forEach { throw it.e }
return try {
body(args.filterIsInstance<Either.Success<Any>>().map { it.value }) as Either.Success
} catch (e: Exception) {
Either.Error(e.message, e)
}
}
Now this block can be used like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
object Test {
private fun echo(name: String): Either<String> {
if (name == "ashish2") throw Exception("name: ashish2 is not allowed here")
return Either.Success(name)
}
private fun getRandomInteger(): Either<Int> {
return Either.Success((Math.random() * 1000).toInt())
}
private fun execute(): String? {
val result = attempt {
val helloAshish = attempt { echo("ashish") }
val randomInt = attempt { getRandomInteger() }
val helloCombined = attempt(helloAshish, randomInt) {
echo((it[0] as String) + (it[1] as Int).toString())
}
helloCombined
}
return when (result) {
is Either.Success -> result.value
is Either.Error -> result.message
}
}
@JvmStatic
fun main(args: Array<String>) {
println(execute())
}
}
Notice the use of nested attempt
calls in execute
. If any of those calls raise an exception, the value returned from that call becomes Either.Error(message: String?, e: Exception)
. It depends on the outer block to handle it appropriately. We are required to handle it at least once in the when
block in line 20. In case the value of a computation depends on whether the preceding computations were successful or not, attempt
takes those previous results and forwards the success values to its code block as a list (line 16) if all of those are not Either.Error
. If any of them is an Either.Error
, attempt
assigns the same exception to helloCombined
, which becomes Either.Error
itself. Finally, helloCombined
is returned as the final result, and then is handled appropriately in line 20.
Also notice that we do not need to specify Either<T>
as the type for attempt
calls. Kotlin is smart enough to guess that from the return type of the lambda.
The above code produces the following output:
ashish279
If we change the line 13 to val helloAshish = attempt { echo("ashish2") }
, we raise an exception in line 3, and the final output becomes:
name: ashish2 is not allowed here