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 articles, among 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Β Result
s, 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 Result
s 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 OutOfMemory
, ThreadDeath
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.