If you don’t get the title - don’t worry, you will. 1.3-M1 and even 1.3-M2 version of Kotlin was released and it contains pretty sweet changes. Ok, a few pretty sweet changes and a few that are either not groundbreaking or weird. The weird one is SuccessOrFailure. I read a lot about it and this post sums it up along with my thoughts.

Glossary: KEEP - Kotlin Evolution and Enhancement Process, document describing potential improvements for Kotlin

I’m not trying to be smarter than guys that wrote KEEP for SuccessOrFailure, so if you need comprehensive understanding of the rationale - look at this KEEP and its (very deceivingly named) discussion: Encapsulate successful or failed function execution

In short

As an introduction, SuccessOrFailure is similar to Try - it represents either success and corresponding value or failure and corresponding exception.

How it works:

These samples are not how SuccessOrFailure was intended to be used by its designers. Don’t use it like that, there are alternatives like Try in Arrow or Kotlin nullable types. (I know, it’s tempting.)

Let’s create a simple function toInteger which returns SuccessOrFailure<Int> - it tries to parse integer from a string.

fun toInteger(s: String): SuccessOrFailure<Int> {
    return try {
        SuccessOrFailure.success(Integer.parseInt(s))
    } catch (e: Exception) {
        SuccessOrFailure.failure(e)
    }
}

Then we can use it like that:

if (toInteger(".").isFailure) println("Failed")
if (toInteger("3").isSuccess) println("Didn't fail")

// conversion from exception to nullable type
val number: Int? = toInteger("23").getOrNull()

// from SuccessOrFailure to regular error handling
try {
    toInteger("not a number").getOrThrow()
} catch (e: Exception) {
    println("Failed with $e")
}

// functional error handling
toInteger("42").fold(
        { println("converted number is $it")},
        { println("conversion failed") }
)

toInteger("42")
        .onSuccess { println("converted number is $it")}
        .onFailure { println("conversion failed") }

// default value in case of error
val n1: Int = toInteger("42").getOrElse { e -> if (e is NumberFormatException) 0 else -1 }

// default value in case of error - rethrowing exception in function
val n2: SuccessOrFailure<Int> = toInteger("42").recover { e -> 0 }

// default value in case of error - catching exception in function
val n3: SuccessOrFailure<Int> = toInteger("42").recoverCatching { e -> Integer.parseInt("-1") }

// Optional-monad-like mapping
toInteger("42").map { n -> n + 2 }

// Optional-monad-like mapping with catching exception
toInteger("42").mapCatching { n -> n + Integer.parseInt("-1") }

As you can see, pretty sweet! When I was writing this code I really enjoyed it and it’s even sadder for me that it should not be used like that.

Why?

Again, there is a robust reasoning in KEEP but for shorter version:

  • Continuation interface, which is used in coroutines would work better with only one method. Currently it has two: fun resume(value: T) and fun resumeWithException(exception: Throwable). Now it will be unified into fun resumeWith(result: SuccessOrFailure<T>)
  • Asynchronous operations that throw errors are tricky - in some cases in coroutines first exception might stop execution of others. Other times, final output is dependent on multiple factors, including how many tasks failed and how they did it. To avoid that, we don’t want to throw regular exception but we still want to have some result of parallel computation. That’s where SuccessOrFailure comes in handy. For nice code example see here
  • Easier error handling of multiple tasks and potentially mapping its results. Again, good sample can be found in KEEP.
  • Functional error handling is getting more popular and mixing try-catch with functional approach looks bad. While Kotlin designers don’t explicitly say that functional is better than procedural, they admit that functional error handling definitely has its place.

When?

And now comes the biggest thing: SuccessOrFailure - even though works great (ok, flatMap also would be handy) for functional error handling - should not be used that way. Hell, the name was made that long and ‘inconvenient’ so people would not use it too often!

This is definitely strange design decision - it might cause SuccessOrFailure to be abused anyway, but it will also look bad. Time will tell if people will listen to language designers and not use it for general error handling. Important question is: should programmers listen to language authors opinion if class works well in some case?

What is the most important - SuccessOrFailure was meant to be used mainly in context of coroutines and several tasks that should not interrupt processing and/or should return information about this task being successful.

Using it as return value and general error handling is discouraged. This is not the tool for this kind of job - it might come in the future versions of Kotlin.

Srsly? So how should I handle exceptions in Kotlin?

This topic deserves separate post - coming soon!

But I REALLY want to use it as returned value

In next post I will write about alternatives which you should really think over. But if you still insists, there are a few things in KEEP that you should keep (pun intended) in mind:

  • SuccessOrFailure should catch all exceptions, not only one type
  • It should be used when method will be used as one of multiple operations of this kind and in multiple places - if it is used like that in one place, this method can be wrapped in runCatching { })
  • There should be usual method throwing exception and only additional method with suffix *Catching should return SuccessOrFailure

So success or failure?

The more I use Kotlin data classes, the less I feel the need for Try-like construct. BUT for times when there is sequential flow and every step might fail, having flatMapable Try would come very handy. It’s hard to say SuccessOrFailure is bad - for what is its purpose, it will probably server well. So it’s good. At the same time I believe Kotlin language designers should really consider introducing something like Try. It would make easier for Kotlin users to choose functional paradigm without resorting to third-party libraries like Arrow.