πŸš€ Β· kotlin.Result

5 min read Β· Updated on by

Read on for a short tour of the most important standard library functions for working with Result β€” general transformation using fold(), retrieving values using getOrThrow()getOrElse()/getOrDefault(), mapping success using map()/mapCatching(), mapping failure using recover()/recoverCatching() and peeking using onSuccess()/onFailure().

The kotlin.Result type was added to the standard library to solve the issues we talked about in the previous articlesamong others. Its implementation is a little different from the one we presented in the previous section β€” it is not a sealed class hierarchy, but instead implemented as a single value class.

Other than that, it is mostly the same as what we designed ourselves β€” it is still semantically equivalent to Result<T> = Success(T) | Failure, even though it is not implement that way.

To manually create a success or failure value, use the Result.success(value: T) and Result.failure(exception: Throwable) methods defined on the companion object. You can determine if the Result is a success or failure using the isSuccess and isFailure methods, and access the value/Throwable using getOrNull() and exceptionOrNull(). However, you will rarely need these, since they promote a more imperative style of programming, and should instead prefer one of the standard functions described bellow.

The standard library also defines a runCatching method, which is exactly the same as what we used before:


//sampleStart
fun doSomeStuff(argument: Int): Result<Int> = runCatching {
    // stuff that can throw
}
//sampleEnd

There is also a receiver version, T.runCatching. Basically, T.runCatching is to T.run what runCatching is to run:


//sampleStart
// findById as defined in org.springframework.data.repository.CrudRepository returns Optional
fun retrieve(id: ProductId): Result<PersistedProduct> = productRepository.runCatching { findById(id).get() }
//sampleEnd

Standard functions

All the standardized functions are specialized versions of the following function:


//sampleStart
inline fun <T, R> Result<T>.fold(
    onSuccess: (value: T) -> R,
    onFailure: (exception: Throwable) -> R
): R
//sampleEnd

This transforms the value contained in the Result, depending on whether it is a success or failure. In essence, the function returns onSuccess(this.getOrNull()!!) if the Result is a success, and onFailure(this.exceptionOrNull()!!) otherwise.

Retrieving a value

getOrThrow()


//sampleStart
fun <T> Result<T>.getOrThrow(): T
//sampleEnd

Returns the success value, or throws. While this might seem like a trivial function, it is actually incredibly useful when composing methods that returnΒ Results, and the subject is interesting enough that we'll dedicate a separate article to it.

Equivalent to fold({ it }, { throw it }).

getOrElse(), getOrDefault()


//sampleStart
inline fun <T, R> Result<T>.getOrElse(
    onFailure: (exception: Throwable) -> R
): R 
fun <T, R> Result<T>.getOrDefault(defaultValue: R): R
//sampleEnd

Returns the success value, or maps the exception to one/returns a default value:


//sampleStart
fun Result<T>.toResponseString() = getOrDefault("An exception occurred!")
fun Result<T>.toDetailedResponseString() = getOrElse { "An exception occurred: $it" }
//sampleEnd

Equivalent to fold({ it }, ::onFailure)/fold({ it }, { defaultValue })

Mapping success Results

map()


//sampleStart
inline fun <T, R> Result<T>.map(
    transform: (value: T) -> R
): Result<R>
//sampleEnd

Transforms a success value. Exceptions thrown inside transform get re-thrown.


//sampleStart
// Assume the 'retrieve' implementation from above 
fun priceOf(id: ProductId): Result<PriceCZK> = productService.retrieve(id).map { it.price }
//sampleEnd

Equivalent to


//sampleStart
// Assume the 'retrieve' implementation from above 
fold({ Result.success(transform(it)) }, { Result.failure(it) })
//sampleEnd

mapCatching()


//sampleStart
inline fun <T, R> Result<T>.mapCatching(
    transform: (value: T) -> R
): Result<R>
//sampleEnd

Transforms a success value, converting any exception thrown in transform to a failure.


//sampleStart
// Assume 'salesService' and 'salesRepository' are defined similarly to their 'product' counterpart
fun getProductName(id: SaleId): Result<String> = salesService
    .retrieve(id) // returns Result<PersistedSale>
    .mapCatching { 
        salesRepository
            .findById(it.product.id) // Returns Optional<ProductEntity>
            .get()
            .name 
    }
//sampleEnd

Equivalent to


//sampleStart
// Assume the 'retrieve' implementation from above 
fold({ runCatching { transform(it) } }, { Result.failure(it) })
//sampleEnd

Which should you use?

Only use map/mapCatching when transforming a single Result. We’ll talk about combining and composing multiple Results in the next article.

Use map if the business/technical essence of the transformation cannot cause failure (e.g. multiplying a value by 2). Use mapCatching if the essence of the transformation can cause failure (i.e. saving a record, performing a business calculation, etc).

I’m emphasizing the essence of the transformation, because, in theory, every single line can throw an OutOfMemoryThreadDeath etc. error. There’s really no point in wrapping every single line in a runCatching β€” these errors, if they really happen while doing something as trivial as multiplying by two, will get caught by one of the β€œupstream” callers of the method.

Mapping failure Results

recover()


//sampleStart
// Assume the 'retrieve' implementation from above 
inline fun <T, R> Result<T>.recover(
    transform: (exception: Throwable) -> R
): Result<R>
//sampleEnd

Transforms a failure value (R needs to be assignable to T). Similar to map, but operates on failures. Like map, exceptions thrown inside transform get re-thrown.


//sampleStart
fun createOrUpdate(product: Product) = 
	productService.update(product) // Returns Result.failure when product is not persisted
   		.recover { productRepository.create(product) }
//sampleEnd

Equivalent to


//sampleStart
// Assume the 'retrieve' implementation from above 
fold({ Result.success(it) }, { Result.success(transform(it)) })
//sampleEnd

recoverCatching()


//sampleStart
inline fun <T, R> Result<T>.recoverCatching(
    transform: (exception: Throwable) -> R
): Result<R>
//sampleEnd

Transforms a failure value, converting any exception thrown in transform to a failure. Similar to mapCatching, but operates on failures.


//sampleStart
// Assume that a Sale contains a reference to the Product that was sold. A Sale can only be 
// persisted if it references a product that was already persisted
fun createOrUpdate(sale: Sale) = 
    saleService.update(sale) // Returns failure when sale is not persisted
    	.recoverCatching { // Throws exception when sale.product is not persisted
            saleRepository.create(sale) 
        }
//sampleEnd

Which should you use?

As with map, use recover if the transformation is atomic and/or the business/technical essence of the transformation doesn't include failure. Use recoverCatching if the transformation is complex, and/or the essence of the transformation includes failure.

Peeking

onSuccess(), onFailure()


//sampleStart
inline fun <T> Result<T>.onSuccess(
    action: (value: T) -> Unit
): Result<T>
inline fun <T> Result<T>.onFailure(
    action: (exception: Throwable) -> Unit
): Result<T>
//sampleEnd

Executes action if Result is a success/failure. Returns the original Result unchanged. These functions are basically the equivalent of also, but in the context of Result. Their use-cases are the same.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer