- 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): Data
This 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
readFileCatching
to make it explicit to the caller that all encountered failures were caught and encapsulated inResult
and 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 returnsSingle
does not actually do anything untilsubscribe
is 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
doSomethingAsync
invocation 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/catch
has different semantics, since it also catches exceptions that could have been thrown byprocessData
. Preserving functional-style error-handling semantics usingtry/catch
is 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
resume
andresumeWithException
to 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
OrThrow
suffix likegetOrThrow
. - Functions that capture thrown exception and encapsulate it into
Result
instance are named with explicitCatching
suffix likerunCatching
andmapCatching
. - A traditional
map
transformation function that works on successful cases is augmented with arecover
function that similarly transforms exceptional cases. A failure inside eithermap
orrecover
transform aborts operation like a traditional function, butmapCatching
andrecoverCatching
encapsulate failure in transform into the resultingResult
. - Functions to query the case are naturally named
isSuccess
andisFailure
. - Functions that act on the success or failure cases are named
onSuccess
andonFailure
and return their receiver unchanged for further chaining according to tradition established byonEach
extension 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> // ERROR
However, 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>) // Ok
Functions 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> // Ok
The 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!!) // ERROR
The 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? // Ok
If 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): FindUserResult
Exceptions 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): User
This 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 t
in 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
runCatching
function and otherCatching
functions 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: Throwable
type parameter that must be specified and another one without it. Moreover, specifyingE
on 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 typesL
andR
and then usingtypealias Result<T> = Either<Throwable, T>
raises similar problems with an additional burden of designing functions forEither
that would not needlessly pollute the namespace of functions applicable toResult
. We don't have sufficient motivating use-cases for havingEither
in the Kotlin Standard Library beyond theoretical desire to baseResult
upon 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
Success
case is marked with@JvmInline
annotation here. That is the case that should be represented without boxing. In general, if@JvmInline sealed value
classes are allowed in the future, then Kotlin compiler could only support@JvmInline
annotation on a set of subclasses with pairwise non-intersecting types of their primary constructor properties. In particular, bothSuccess
andFailure
cannot be@JvmInline
at 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
Result
type 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
Result
type from functions. - Allow Kotlin null-safety operators
?.
,?:
and!!
on both nullable and non-nullResult
types. - Text updated to replace
inline class
with@JvmInline value class
.
- Allow returning the