REDIRECT TO: https://github.com/Kotlin/KEEP/blob/main/proposals/stdlib/KEEP-0127-result.md
- Type: Standard Library API proposal
- Author: Roman Elizarov
- Contributors: Andrey Breslav, Ilya Gorbunov
- Status: Implemented in Kotlin 1.3
- Revisions: Updated in Kotlin 1.5, see revision history.
- Related issues: KT-18608
- Original discussion: KEEP-127
Kotlin provides exceptions that are used to represent an arbitrary failure of a function and include ability to attach additional information pertaining to this failure. Exceptions are sequential in nature and work great in any kind of sequential code, including code for a single coroutine or in other case where one piece of work in being sequentially decomposed. Exceptions ensure that the first failure in a sequentially performed work stops further progress and is propagated up to the caller. However, sequential nature of exceptions complicates their use in cases where some kind of parallel decomposition of work is needed or multiple failures need to be retained for later processing.
We'd like to introduce a type in the Kotlin standard library that is effectively a discriminated union between successful
and failed outcome of execution of Kotlin function — Success T | Failure Throwable,
where Success T represents a successful result of some type T
and Failure Throwable represents a failure with any Throwable exception.
For the purpose of efficiency, we would model it as a generic @JvmInline value class Result<T>
in the standard library.
NOTE: In Kotlin 1.3 till 1.5 this Result could not be used directly as a return type of Kotlin functions.
This restriction was lifted in Kotlin 1.5.
See limitations section for details.
See also style and exceptions and
use cases below on how Result is designed to be used.
This section lists motivating use-cases.
The primary driver for inclusion of this class into the Standard Library is Continuation<T> callback interface
that should get invoked on the successful or failed execution of an asynchronous operation.
We'd like to be able to have only a single function with "success or failure" union type as its parameter:
interface Continuation<in T> {
fun resumeWith(result: Result<T>)
}Another example here is parallel execution of multiple asynchronous operations that must capture successful or failed execution of each individual piece to analyze and reach decision on the outcome of a larger piece of work:
val deferreds: List<Deferred<T>> = List(n) {
async {
/* Do something that produces T or fails */
}
}
val outcomes1: List<T> = deferreds.map { it.await() } // BAD -- crash on the first (by index) failure
val outcomes2: List<T> = deferreds.awaitAll() // BAD -- crash on the earliest (by time) failure
val outcomes3: List<Result<T>> = deferreds.map { runCatching { it.await() } } // !!! <= THIS IS THE ONE WE WANT Kotlin encourages writing code in a functional style. It works well as long as business-specific failures are represented with nullable types or sealed class hierarchies, while other kinds of failures (that are represented by exceptions) do not require any special local handling. However, when interfacing with Java-style APIs that rely heavily on exceptions or otherwise having a need to somehow process exceptions locally (as opposed to propagating them up the call stack), we see a clear lack of primitives in the Kotlin standard library.
Consider writing a function readFiles that receives a list of files, reads all of them, and returns a
list of results. We are given the following function to read single file contents:
fun readFileData(file: File): DataThis reading function throws exception if file is not found or parsing of a file had somehow failed. Normally that would
be fine, and the first failure of this kind would terminate the whole program with a stacktrace and explanatory message.
However, for readFiles we'd explicitly like to be able to continue after the failure to collect and report all failures.
Moreover, we'd like to be able to have a functional implementation of readFiles like this:
fun readFilesCatching(files: List<File>): List<Result<Data>> =
files.map {
runCatching {
readFileData(it)
}
}This function is named
readFileCatchingto make it explicit to the caller that all encountered failures were caught and encapsulated inResultand it is caller responsibility to process these failures.
Now, consider making some transformation of readFilesCatching results that we'd like to express functionally,
while preserving accumulated failures:
readFilesCatching(files).map { result: Result<Data> -> // type explicitly written here for clarity
result.map { it.doSomething() } // Operates on Success case, while preserving Failure
}If doSomething, in turn, can potentially fail and we are interested in keeping this failure per each individual
file, then we can write it using mapCatching instead of map:
readFilesCatching(files).map { result: Result<Data> ->
result.mapCatching { it.doSomething() }
}In mostly functional code try { ... } catch(e: Throwable) { ... } construct looks
out of style. For example, consider this piece of code that uses RxKotlin
for asynchronous processing.
It invokes doSomethingAsync that returns
Single and processes potential error in a functional style:
doSomethingAsync()
.subscribe(
{ processData(it) },
{ showErrorDialog(it) }
)Note, that the above code is written in a style that is very different from direct programming style.
doSomethingAsync()that returnsSingledoes not actually do anything untilsubscribeis invoked (its result is typically cold). This distinction is not important for the purposes of this section. We are interested here in a visual fact that error and result handling are chained to the initial invocation.
Working with function that returns Java's CompletableFuture
is visually similar:
doSomethingAsync()
.whenComplete { data, exception ->
if (exception != null)
showErrorDialog(exception)
else
processData(data)
}It is closer to direct style, since this
doSomethingAsyncinvocation actually starts performing operation, but we also see that ultimate processing of success or failure is performed via chaining.
Now, if doSomethingSync is a synchronous function, then handling its success or failure looks quite visually different,
which is problematic for the code that mixes both approaches:
try {
val data = doSomethingSync()
processData(data)
} catch(e: Throwable) {
showErrorDialog(e)
}Also note, that the code with
try/catchhas different semantics, since it also catches exceptions that could have been thrown byprocessData. Preserving functional-style error-handling semantics usingtry/catchis quite non-trivial (see Error handling alternative section).
Instead, we'd like to be able to write the same code in a more functional way:
runCatching { doSomethingSync() }
.onFailure { showErrorDialog(it) }
.onSuccess { processData(it) }There is a number of community-supported libraries that provide this kind of success or failure union type,
but we cannot use any of them for the Continuation callback interface that is defined in the Standard Library.
Alternative signatures for the Continuation interface are listed below.
Two methods as in current experimental version of coroutines:
interface Continuation<in T> {
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}This solution was tried in the experimental version of Kotlin coroutines and the following problems were identified:
- All implementations have to implement both methods and there is no easy shortcut to provide a builder with
a lambda like
Continuation { ... body ... }. - Some implementations need to capture "success or failure" in their state and pass on captured success or failure to another delegate continuation at a later time.
- Some implementations have a common piece of logic that should be executed on both success and failure
with minor differences for successful and failed cases. These implementations have to immediately forward both
resumeandresumeWithExceptionto some internal function likedoResume, thus increasing stack size and still forcing implementor to figure out a way to represent both success and failure in one method.
One method with two parameters:
interface Continuation<in T> {
fun resume(value: T?, exception: Throwable?)
}The downside here is that both parameters here are nullable and there is no larger type-safety nor a clear indication of intent to have only one of them set.
One method with Any? parameter:
interface Continuation<in T> {
fun resume(result: Any?) // result: T | Failure(Throwable)
}This solution completely lacks any type-safety on Kotlin side.
Let's see what it takes to rewrite the code with functional-style error handling without resorting to 3rd party libraries.
Non-nullable value type:
If the result of doSomethingSync is non-nullable, then we can write somewhat concise code:
val data: Data? = try {
doSomethingSync()
} catch(e: Throwable) {
showErrorDialog(e)
null
}
if (data != null)
processData(data) Nullable value type:
If the result of doSomethingSync is nullable, then one possible alternative is shown below:
var data: Data? = null
val success = try {
data = doSomethingSync()
true
} catch(e: Throwable) {
showErrorDialog(e)
false
}
if (success)
processData(data) The following snippet gives summary of all the public APIs:
@JvmInline value class Result<out T> /* internal constructor */ {
val isSuccess: Boolean
val isFailure: Boolean
fun getOrNull(): T?
fun exceptionOrNull(): Throwable?
companion object {
fun <T> success(value: T): Result<T>
fun <T> failure(exception: Throwable): Result<T>
}
}
inline fun <R> runCatching(block: () -> R): Result<R>
inline fun <T, R> T.runCatching(block: T.() -> R): Result<R>
fun <T> Result<T>.getOrThrow(): T
fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R
inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R
inline fun <R, T> Result<T>.fold(onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R): R
inline fun <R, T> Result<T>.map(transform: (value: T) -> R): Result<R>
inline fun <R, T: R> Result<T>.recover(transform: (exception: Throwable) -> R): Result<R>
inline fun <R, T> Result<T>.mapCatching(transform: (value: T) -> R): Result<R>
inline fun <R, T: R> Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R>
inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T>
inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T>The functions have self-explanatory consistent names that follow established tradition in the Kotlin Standard library and establish the following additional conventions:
- Functions that can throw previously suppressed (captured) exception are named
with explicit
OrThrowsuffix likegetOrThrow. - Functions that capture thrown exception and encapsulate it into
Resultinstance are named with explicitCatchingsuffix likerunCatchingandmapCatching. - A traditional
maptransformation function that works on successful cases is augmented with arecoverfunction that similarly transforms exceptional cases. A failure inside eithermaporrecovertransform aborts operation like a traditional function, butmapCatchingandrecoverCatchingencapsulate failure in transform into the resultingResult. - Functions to query the case are naturally named
isSuccessandisFailure. - Functions that act on the success or failure cases are named
onSuccessandonFailureand return their receiver unchanged for further chaining according to tradition established byonEachextension from the Standard Library.
String representation of the Result value (toString) is either Success(v) or Failure(x) where v and x are
the string representations of the corresponding value and exception. equals and hashCode are implemented
naturally for the result type, comparing the corresponding values or exceptions.
This library depends on
@JvmInline value class
language feature for its efficient implementation.
In versions before Kotlin 1.5 Result<T> cannot be used as a direct result type of Kotlin functions, properties of
Result type are also restricted:
fun findUserByName(name: String): Result<User> // ERROR: 'kotlin.Result' cannot be used as a return type
fun foo(): Result<List<Int>> // ERROR
fun foo(): Result<Int>? // ERROR
var foo: Result<Int> // ERRORHowever, functions that use Result type in generic containers or receive result as a parameter type
were allowed:
fun findIntResults(): List<Result<Int>> // Ok
fun receiveIntResult(result: Result<Int>) // OkFunctions that declare generic result types may, in fact, return values of Result type when the
Result type is substituted in place of their generic type parameters:
private val first: Result<Int> = findIntResults().first() // Ok, even though `first` is of type Result<Int>Private and local properties of Result type were allowed as long as they did not have custom getters:
private var foo: Result<Int> // OkThe use of Kotlin null-safety operators ?., ?: and !! was not allowed on both nullable and non-null Result types:
val r: Result<String?> = runCatching { readLine() }
println(r!!) // ERRORThe rationale behind these limitations was that future versions of Kotlin might wish to expand and/or change semantics
of functions that return Result type and null-safety operators may change their semantics when used
on values of Result type. In order to avoid breaking existing code in the future releases of Kotlin and leave door open
for those changes, the corresponding uses produced an error. Exceptions to this rule were made for carefully-reviewed
declarations in the standard library that are part of the Result type API itself.
UPDATE: These limitations are lifted since Kotlin 1.5.
See Future advancements for details on specific plans on updates on them.
Result<T> is implemented by an @JvmInline value class and is optimized for a successful case. Success is stored as
a value of type T directly, without additional boxing, while failure exception is wrapped into an internal
Result.Failure class that is not exposed through binary interface and may be changed later.
Result class has the following internal published APIs that
represent its binary interface on JVM in addition to its public API:
@JvmInline value class Result<out T> @PublishedApi internal constructor(
@PublishedApi internal val value: Any? // internal value -- either T or Failure
) : Serializable
@PublishedApi internal fun createFailure(exception: Throwable): Any
@PublishedApi internal fun Result<*>.throwOnFailure()The Result class is designed to capture generic failures of Kotlin functions for their latter processing and
should be used in general-purpose API like futures, etc, that deal with invocation of Kotlin code blocks and
must be able to represent both a successful and a failed result of execution. The Result class is not
designed to represent domain-specific error conditions.
In general, if some API requires its callers to handle failures locally (immediately around or next to the invocation), then it should use nullable types, when these failures do not carry additional business meaning, or domain-specific data types to represent its successful results and failures with any additional business-related data that is needed to process these failures.
Consider this hypothetical API design:
fun findUserByName(name: String): Result<User> // ERROR If the only kind of failure we might be interested in handling is the failure to find the user with the given name, then the following signature shall be used:
fun findUserByName(name: String): User? // OkIf there is a business need to distinguish different failures and process these different failures in distinct ways on each invocation site, then the following kind of signature shall be considered:
sealed class FindUserResult {
data class Found(val user: User) : FindUserResult()
data class NotFound(val name: String) : FindUserResult()
data class MalformedName(val name: String) : FindUserResult()
// other cases that need different business-specific handling code
}
fun findUserByName(name: String): FindUserResultExceptions in Kotlin are designed for the failures that usually do not require local handling at each call site.
This includes several broad areas — logic and programming errors like index bounds problems and various checks
for internal invariants and preconditions, environment problems, out of memory conditions, etc.
These failures are usually non-recoverable (or are not supposed to be recovered from) and are handled in some
centralized way by logging or otherwise reporting them for troubleshooting, typically terminating application
or, sometimes, attempting to restart or to reinitialize an application as a whole or just its failing subsystem.
This is where default exceptions behaviour to abort current operation and propagate it up the call stack comes in handy.
External environment problems like network or file input/output errors represent a corner case here.
It is cumbersome to require their local handling by the caller as it complicates sequential business
logic by obscuring it with code to handle IO errors, so it is idiomatic in Kotlin to use exceptions (like IOException)
for these. However, they are often handled at a more granular level than some global error-handling code.
These errors often require some specific user-interaction and can require domain-specific retry or recovery code.
Exceptions are also very expensive to create, but relatively cheap to throw, because they carry a lot of additional metadata, like stack trace and message to aid in debugging. They are extremely valuable when this metadata is written to the log for developers to aid in troubleshooting, but all that metadata is useless if exception is to be consumed by some business-logic to make some business-decision based simple on the presence of exception. Use nullable types or domain-specific classes to represent failures that need specific handling.
So, in case when findUserByName failure does not require local handling by the caller, then its failure
should be represented by exception and its signature should look like this:
fun findUserByName(name: String): UserThis signature is fine if we always sure that user shall be found, unless we have bugs, environment or IO issues.
If invoker of this function wants to perform multiple operations and process their failures afterwards
(without aborting on the first failure), it can always use runCatching { findUserByName(name) }
to make it explicit that a failure is being caught and
encapsulated into Result instance.
Kotlin Standard Library provides rich collection of transformations for nullable types that are idiomatic in Kotlin to indicate failure when no additional information about the failure is needed. However, there is no build-in support for non-standard exception handling in the Standard Library -- exceptions always terminate operation and propagate to the caller.
Other programming languages include a similar facility to represent a union of success and failure in their standard library with the following names:
Try[T]in Scala is similar to the proposedResult<T>.Result<T, E>in Rust (also parametrized by the type of error).Exceptional e tin Haskell (also parametrized by the type of error).expected<E, T>in C++ (proposed, also parametrized by the type of error).
Existing Kotlin libraries that provide similar functionality:
Try<T>from Arrow library.Result<T, E>from @kittinunf.
Note, that both of the libraries above promote "Railway Oriented Programming" style with monads and their transformations, which heavily relies on functions returning
Try,Result, etc. This programming style can be implemented and used in Kotlin via libraries as the above examples demonstrate. However, core Kotlin language and its Standard Library are designed around a direct programming style in mind. The general approach in Kotlin is that alternative programming styles should be provided as 3rd party libraries and DSLs.
For a more detailed comparison of Scala's Try and its Kotlin analogue in Arrow library with this Result class
see Appendix.
This API shall be placed into the Kotlin Standard Library. Since the proposed API is fairly small and does not
clearly belong to any larger group of APIs, it should be placed directly into kotlin package.
This section lists open issues about this design.
Parameterizing this class by the type of exception like Result<T, E> is possible, but raises the
following problems:
- It increases verboseness without providing improvement for any of the outlined Use cases.
- Kotlin currently lacks facility to specify default values for generic type parameters.
- It leads to abuse in cases where a user-provided API-specific sealed class would work better.
It is possible to define a separate class like ResultEx<T, E> that is parametrized by
both successful type T and failed type E (that must extend Throwable)
and then define Result<T> and a typealias to ResultEx<T, Throwable>.
However, this creates its own problems:
- Typealiases are quite verbosely rendered by IDE in signatures and there is no clear way on making them better.
- We cannot succinctly define
runCatchingfunction and otherCatchingfunctions to make them usable both with and without explicit caught type specification. We'll have to have two different names for such a function: one for a function with an additionalE: Throwabletype parameter that must be specified and another one without it. Moreover, specifyingEon call site requires specifying return type, too, since partial type parameter specification is not currently possible in Kotlin.
All in all, it does not seem that the costs outweigh whatever benefits it might bring.
Defining an even more general
Either<L, R>type as a discriminated union between two arbitrary typesLandRand then usingtypealias Result<T> = Either<Throwable, T>raises similar problems with an additional burden of designing functions forEitherthat would not needlessly pollute the namespace of functions applicable toResult. We don't have sufficient motivating use-cases for havingEitherin the Kotlin Standard Library beyond theoretical desire to baseResultupon it.
Using Result as the return type of Catching functions poses a problem that it might accidentally get lost,
thus losing unhandled exception.
Consider this code from Functional bulk manipulation of failures:
readFilesCatching(files).map { result ->
result.map { it.doSomething() }
}If doSomething here throws an exception, then all exceptions that were returned in a list by readFilesCatching are lost.
Some IDE inspections can be designed to detect these kinds of problems. It is an open question how exactly they should work and and whether it is really a big problem after all.
API for Result class is designed to be quite bare-bones.
However, according to Functional bulk manipulation of failures use-case,
one might occasionally encounter List<Result<T>> or another collection of Result instances.
It is open question whether we should provide additional extensions in the Standard Library to represent common
operations on such collections and what those operations might be.
This section lists potential directions for future enhancement. None of them are worked out at the moment and all of them are purely tentative.
Kotlin @JvmInline value classes cannot be currently used with sealed class construct.
If that is supported in the future, then we could change implementation of
Result without affecting its public APIs and binary interfaces in the following way:
@JvmInline sealed value class Result<T> {
@JvmInline class Success<T>(val value: T) : Result<T>()
class Failure<T>(val exception: Throwable) : Result<T>()
}Notice, that only
Successcase is marked with@JvmInlineannotation here. That is the case that should be represented without boxing. In general, if@JvmInline sealed valueclasses are allowed in the future, then Kotlin compiler could only support@JvmInlineannotation on a set of subclasses with pairwise non-intersecting types of their primary constructor properties. In particular, bothSuccessandFailurecannot be@JvmInlineat the same time, since we would not be able to distinguishSuccess(Exception(...))fromFailure(Exception(...))at run time.
These changes would make it possible to use result is Success and result is Failure expressions and get advantage of
smart casts instead of result.isSuccess and result.isFailure that are currently provided and which do not work
with smart casts.
If Kotlin adds some form of support for type parameter default values and partial type inference,
then we can consider extending Result class with an additional type parameter E: Throwable that represents
the base class for caught exceptions. For example, in input/output code there may be a desire to catch only
IOException and its subclasses, while aborting on any other exception using something like
runCatching<_, IOException> { code } assuming that return type can be still inferred
(potential partial type inference syntax here is used for illustration only).
Kotlin nullable types have extensive support in Kotlin via operators ?., ?:, !!, and T? type constructor
syntax. We can envision better integration of Result into the Kotlin language in the future.
However, unlike nullable types, that are often used to represent non signalling failure that does not cary
additional information, Result instances also carry additional information and, in general, shall be
always handled in some way. Making Result an integral part of the language also requires a
considerable effort on improving Kotlin type system to ensure proper handling of encapsulated exceptions.
UPDATE: We have reached a decision of not following this road for a foreseeable future and will not pursue integration
of any special constructions into the language that are tied to the Result type. As a part of the standard library
a Result type will stay narrowly-focused on the use-cases that are described at the beginning of this document,
abandoning ambitions to become any kind of universal error-handling primitive. We are working in other directions
of making signalling error handling more pleasant to use in Kotlin.
The text below is left for historical reasons.
One potential direction is to allow return value of Result type,
so that with parametrization by the base error type one can write:
fun findUserByName(name: String): Result<User, IOException>This declaration would be conceptually equivalent to a Java function that is declared with User
result type and throws IOException annotation.
However, unlike throws annotation in Java, Result<User, IOException> is going to
be considered a return type of this function that explicitly declares exception that must be handled locally.
There will be no silent propagation of that exception type up to the caller. The caller will be
required to handle it explicitly. When one writes:
val result = findUserByName(name)Then inferred type of result will be Result<User, IOException>.
Direct access to the User methods and extensions would not be possible,
but all the ?., ?:, and !! operators can be extended to work appropriately with Result type to
make the corresponding code fluent
in a similar way as it happens with nullable types today. Some additional operators might be required, too.
Unlike checked exception in Java, these are going to be full-blown types, so they play nicely with collections
(List<Result<User, IOException>> is going to be a valid type)
and all the higher-order functions in Kotlin will work properly with those types without the problems
that made it impossible to properly integrate checked exceptions with Java generics.
Moreover, it can be very efficiently implemented on JVM in the return type position by actually throwing the corresponding
exception inside and catching it outside, on the caller side, so no boxing will be required even for primitive
results. "Rethrowing" exceptions with !! can be transparent in JVM bytecode in the same way as it
happens in Java programs using exceptions.
Update note: This proves virtually impossible to implement correctly, as there would be no way to distinguish a domain-specific error that needs to be represented in the
Resulttype for handling it by the caller from the logic error in the code (a crash) that shall still be represented as exception to be handled in a centralized place of the application for logging, etc.
All in all, it could provide a safe replacement for checked exceptions on JVM and open a path to a better
integration with JVM APIs that rely on checked exceptions. However, details of this interoperability will have to
be worked out as there are lots of problems down this path. We cannot just lift all Java functions with throws into
Kotlin functions with the corresponding Result type not only because of backwards compatibility, but also due to the way checked
exceptions are (ab)used in the JVM ecosystem, so are more fine-grained control for interoperability will have to
be designed.
It is all beyond the scope of this KEEP.
You can skip this appendix is you are not familiar with Scala's or Arrow's Try monad that provides very
similar functionality to this Result class.
If you are familiar with Try monad, then you might ask why there is no flatMap
function on the Result class. This function could have been defined with the following signature:
inline fun <R, T> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R>The usual reason to have flatMap is to avoid "nesting" of monadic types when combining multiple
functions that return them, like in the following example:
runCatching { d.await() }.map { it.doSomethingCatching() } // : Result<Result<Data>> -- oops!Functional code that uses Try monad gets quickly polluted
with flatMap invocations. To make such code manageable, a functional programming language is usually extended
with monad comprehension syntax to hide those flatMap invocations.
Take a look at the following example code that
uses monad comprehension over Try monad
(which is adapted from a guide
here):
def getURLContent(url: String): Try[Iterator[String]] =
for {
url <- parseURL(url) // here parseURL returns Try[URL], encapsulates failure
connection <- Try(url.openConnection())
input <- Try(connection.getInputStream)
source = Source.fromInputStream(input)
} yield source.getLines()Adapting functions used here to Kotlin style, one can write this code in Kotlin, with the same semantics of aborting further progress on the first failure, in the following way:
fun getURLContent(url: String): List<String> {
val url = parseURL(url) // here parseURL returns URL, throws on failure
val connection = url.openConnection()
val input = connection.getInputStream()
val source = Source.fromInputStream(input)
return source.getLines()
}Notice, that monad comprehension over Try monad is basically built into the Kotlin language.
That is how imperative control flow works in Kotlin out of the box and there is no need to emulate it
via monad comprehensions. If callers of this function need an encapsulated failure,
they can always use runCatching { getURLContent(url) } expression.
However, the Kotlin is not exactly equivalent to the initial code with Try.
Let us see what are the differences. The original parseURL have been returning an encapsulated exception
and it could be making a fine grained decision on which kinds of exceptions shall be encapsulated into the result
and which kinds of exceptions shall be thrown. Rewritten code propagates any failure in parseURL up to the caller
without this fine grained distinction between different kinds of failures. There is also a subtle difference on
the fromInputStream invocation. Original code would fail with exception if this invocation fails, while any failure
in openConnection and getInputStream is encapsulated into the result of the function via Try. Rewritten code
does not make distinctions between different kinds of failures anymore.
All in all, the differences can be summarized as follows. Result is a blunt tool designed to catch
any failure in the function invocation for the processing later on.
On the other hand, libraries like Arrow provide utility classes like Try and the
corresponding extension functions that enable more fine-grained control. When a function is declared with Try<T>
as its result type, it means that this function can make a fine-grained decision on which failures are encapsulated
and which failures are thrown up the call stack.
If your code needs fine-grained exception handling policy, we'd recommend designing your code in such a way, that
exceptions are not used at all for any kinds of locally-handled failures
(see section on style for example code
with nullable types and sealed data classes). In the context of this appendix, parseURL could return a nullable
result (of type URL?) to indicate parsing failure or return its own purpose-designed sealed class that would provide
all the additional details about failure (like the exact failure position in input string)
if that is needed for some business function
(like setting cursor to the place of failure in the user interface).
In cases when you need to distinguish between different kinds of failures and these approaches do not work for you,
you are welcome to write your own utility libraries or use libraries like Arrow
that provide the corresponding utilities.
- Kotlin 1.5
- Allow returning the
Resulttype from functions. - Allow Kotlin null-safety operators
?.,?:and!!on both nullable and non-nullResulttypes. - Text updated to replace
inline classwith@JvmInline value class.
- Allow returning the