Every program should grapple with failure. A happy path is only a part of the story. There are many ways to handle errors. The essential toolkit showcases throwing and catching exceptions, passing results, leveraging optionals, aborting, and asserting.
Distributed results
Exceptions work well for code that executes sequentially, when you can wait for the results, but there are situations when they are off the menu. For example, in a callback handler that is supposed to evaluate a result (in an inversion of control pattern), emitting the result to a separate reactive stream (error or result handling decoupled from routine execution), or when you want to avoid unhandled exceptions that may crash your program (although crashing is generally good because it exposes a problem). Then comes the Result to save the day.
Note: Using Results makes sense mostly in “dispersed” situations, when the result is handled at a different place from where the routine is called. In co-located circumstances, you can and should use the exceptions as the standard mechanism, possibly with @Throws annotations for better transparency.
Distributed results
When the result handler is far away…
Result represents an outcome of a failable operation.
var asyncOutput: String? = null
var listener: ((Result<Int>) -> Unit)? = { result ->
asyncOutput = result.fold(
onSuccess = { "Input was evaluated to $it" },
onFailure = { "Evaluation failed" }
)
}
fun evalAsync(input: String) {
try {
val value = input.toInt()
listener?.invoke(Result.success(value))
} catch (e: Exception) {
listener?.invoke(Result.failure(e))
}
}