Rich Errors #487
Replies: 43 comments 119 replies
|
I'm curious: do we really need to have a new Based on the document, it feels that we could safely replace it with fun foo(v: Any) {
if (v is Value) { // at runtime: `v !is Error`
}
}So this already implies that fun foo(v: Any) {
if (v !is Error) {
// v is of type `Any \ Error` here
}
}And this case is also covered in negative smart-casting section: fun <T : Any> foo(v: T) {
if (v is Error1) return
// v is smart-casted to T \ Error1
}As well as other examples already show, this notation will be possible. (at the middle of this section) fun <T : Any? \ NoSuchElement> List<T>.firstOrError(): T | NoSuchElementI understand that by doing this, we will stumble upon a problem where we can't do Yes, this will make I don't mind having |
|
Is the following example - a valid usage of the new concept? If so, shouldn't we use a more general term instead of "error"? |
Now that we are using |
I very much dislike making this a compiler error. There are many cases where it is entirely valid to merge errors from different sources. For example, it might be coming from the same underlying service used in different calls. The issue happens when errors are more general than their sources, and whether that's the case or not is a design decision of a particular codebase, not something that can or should be enforced at a language level. |
I like this at first glance, but as the subsequent examples show, it effectively colors generics, and I don't think it's worth that. It's very common, IMO, to just declare unbounded type params where you just need basic parameterization - I know my code is littered with them. This design means that, for example, you wouldn't be able to call any error-returning generic function on them. The declaration would need to essentially opt-in to any errors that could be used, which may not be known or even accessible there. I don't have a great alternative solution, but I do not think requiring every type parameter declare what error types is can work with up front is a workable solution. |
|
It looks like the stdlib, and possible user libraries, will now often end up with three variants of the same function: |
|
What is recoverable (i.e. should be an error) and what isn't (i.e. should be an exception) differs at different layers of applications. The stdlib helpers help with going from error to exception, but not the other way around, and are all or nothing. I made a similar comment on the first KEEP which I think is still relevant here. Easier ways to say "treat this particular exception as an error", "catch the KotlinErrorException for this error", and "treat this particular error as an exception" are still important and still missing. |
|
If it's at all possible, I think it would be very valuable to relax the "no supertypes" restriction for errors. Errors often have their own conceptual hierarchy with associated contracts, e.g. a Even if it's somewhat restricted, e.g. |
|
|
In Proposal -> Design -> Types -> Illustrative examples - The 3rd example looks wrong to me. I don't understand, why graph BT
Neg2["E \ ParseError \ OtherError"]
Neg1["E \ ParseError"]
Neg1'["E \ OtherError"]
Union["E | OtherError"]
Union'["E | ParseError"]
UnionNeg["E | OtherError \ ParseError"]
UnionNeg'["E | ParseError \ OtherError"]
Neg2 --> Neg1
Neg2 --> Neg1'
Neg1' --> E
Neg1' --> UnionNeg'
Neg1 --> UnionNeg
Neg1 --> E
UnionNeg' --> Union'
UnionNeg --> Union
E --> Union
E --> Union'
|
|
Would the
So sounds to me as these would also benefit from the same plugin capabilities. |
|
I like the addition of a non-Error supertype, but why not have it simply be typealias Value = Any / ErrorI'm conflicted on the |
|
I think Alternatively, I believe it can be simplified to the following: inline fun <T : R \ Error, R, E : Error> (T | E).ifError(onError: (E) -> R): R {
contract {
callsInPlace(onError, InvocationKind.AT_MOST_ONCE)
(this@ifError is Error) holdsIn onError
}
return if (this is Error) onError(this) else this
}It feels much more readable to me. inline fun <R, E : Error> ((R \ Error) | E).ifError(onError: (E) -> R): R {
contract {
callsInPlace(onError, InvocationKind.AT_MOST_ONCE)
(this@ifError is Error) holdsIn onError
}
return if (this is Error) onError(this) else this
}I think 2 type parameters is very likely the minimum here, so this is optimal! |
|
There's something to be said about the splitting of parameters like |
|
Just as there's a |
|
I'd like to echo @nikclayton's feedback and also sharpen it a little. KEEPs exist to collect user feedback and review. They have no other purpose, as most Kotlin design work was successfully done without them for many years. When you repeatedly ask long term users for feedback and then ignore it, it's demotivating. I've been using Kotlin since before it even had a website, long before the 1.0 release, and first questioned the recoverable/non-recoverable distinction on YouTrack nearly a year ago. That same concern has been echoed by many other developers since. But it has never been addressed, perhaps because design work was already done and this argument calls into question the value of that work. Yet a strength of Kotlin has always been what it did not include, not only what it did. As Kotlin has become ever more mature it's inevitable that more KEEPs will receive feedback that questions the premises of a proposal, simply because the low hanging obvious fruit everyone can agree on is ever more picked. A symptom of this is that the KEEP makes some very subjective claims, like:
I've used many Kotlin libraries and never encountered custom error types, so I've not personally seen or suffered from any fragmentation or inconsistent ergonomics. I have however suffered from being required to debug Go codebases where error types meant there were no useful stack traces. It would be good to be convinced my experience is atypical with data, but none is forthcoming. There are so many excellent additions that could be made to Kotlin and so few that can ultimately be added. Is a feature with a questionable core premise really the best place to spend your limited budget? |
|
Did you consider open unions to be default or default in the library mode? |
|
To me, the Perhaps a different operator would be preferred? I've thought of E.g. fun fetch(): User | NoSuchUser
fun User.charge(): Transaction | TransactionCancelled
fun foo() {
val transactionResult = fetch()->charge()
if (transactionResult is Error) {
when (transactionResult) {
is NoSuchUser -> println("User not found")
is TransactionCancelled -> println("Transaction cancelled")
}
}
}But unfortunantly, Kotlin already uses fun fetch(): User | NoSuchUser
fun User.charge(): Transaction | TransactionCancelled
fun foo() {
val transactionResult = fetch()\.charge()
if (transactionResult is Error) {
when (transactionResult) {
is NoSuchUser -> println("User not found")
is TransactionCancelled -> println("Transaction cancelled")
}
}
} |
|
Rich Errors currently model alternative failures (A | B | C). Has support for accumulating failures been considered? Validation is a common example where multiple errors are expected simultaneously rather than failing fast. I've read the KEEP, but in the examples provided, a function can return either: an object representing success or any of the errors, but only one of them. Take this for example: While this can already be modeled by introducing another error type that wraps a collection of errors, like this: |
|
I think that using the term I suggest using |
|
We have updated the document after discussions preceding implementation of experimental version:
|
|
Since the design has changed quite a lot, we've also published a document with our analysis of how Rich Errors could be used today in a few projects (DuckDuckGo-Android, Anki-Android, and Signal-Android): Evaluating Rich Errors in the Wild. Feel free to check out the document and share your thoughts or more use cases in the discussion #498 |
|
why not use error class SaveNoteFailure(val message: String? = null)
error class SaveNoteWarning(val message: String?)
suspend fun saveNote(): Unit ! (SaveNoteFailure | SaveNoteWarning) |
|
To follow @Sporking in why the name From the design I've seen, it's more like modeling an alternative flow and the concatenation thereof in monadic types, without having to define a wrapper type for every combination of main flow and (multiple) alternative flow types. Errors aren't the only thing where you'd want to model alternative flows, so calling them error types that are an instance of |
|
While I do understand that the first argument in |
|
Since errors can't implement interfaces, are they expected to be always non-serializable on JVM (not implementing |
|
Has anyone considered treating This would also allow for remaining backwards compatible with the current hierarchy from before rich errors, without needing to introduce the graph BT
NothingError["Nothing | Error"]
IntError["Int | Error"]
Object["Object (Any | Error)"]
NothingNParse["Nothing? | ParseError \n(Nothing | Null | ParseError)"]
AnyNParse["Any? | ParseError \n(Any | Null | ParseError)"]
IntNParse["Int? | ParseError \n(Int | Null | ParseError)"]
NothingParse["ParseError \n(Nothing | ParseError)"]
IntParse["Int | ParseError"]
AnyParse["Any | ParseError"]
NothingN["Nothing? (Nothing | Null)"]
IntN["Int? (Int | Null)"]
AnyN["Any? (Any | Null)"]
Nothing["Nothing"]
Int["Int"]
Any["Any"]
Nothing --> NothingParse
Int --> IntParse
Any --> AnyParse
NothingN --> NothingNParse
IntN --> IntNParse
AnyN --> AnyNParse
subgraph BRE [Before rich errors]
subgraph Value
Nothing ---> Int
Int ---> Any
end
subgraph Nullable value
NothingN ---> IntN
IntN ---> AnyN
end
Nothing --> NothingN
Int --> IntN
Any --> AnyN
end
style BRE fill:#555
subgraph WRE [With rich errors]
NothingParse --> NothingNParse
IntParse --> IntNParse
AnyParse --> AnyNParse
NothingNParse --> NothingError
IntNParse --> IntError
AnyNParse --> Object
subgraph Value or Error
NothingError ---> IntError
IntError --> Object
end
subgraph Value or ParseError
NothingParse ---> IntParse
IntParse ---> AnyParse
end
subgraph Nullable value or ParseError
NothingNParse ---> IntNParse
IntNParse ---> AnyNParse
end
end
style WRE fill:#555
That way:
|
|
Can we have our cake and eat it too? Proposal: If we are to have a whole set of special objects that can be somewhat have union operations, why don't we zoom out a bit of error and implement "sensible" union in kotlin.
This is what I mean to have your cake and eat it too:
Kotlin not only gains new error handling (and alt path handling) but also gains generic unions (with limitations of course but they are there). I think calling them |
|
I was wondering... How would you compose two types together? I mean this regardless of whether For specific cases like But what about something more general? In a previous comment I tried to define an fun <A: Any?, E1: Error, E2: Error> (A|E1).ifNull(default: A|E2): A|E1|E2 {
return if(this == null) default else this
}This has a couple of problems:
If we had the fun <A: Any?, E1: Error, E2: Error \ E1> (A|E1).ifNull(default: A|E2): A|E1|E2 {
return if(this == null) default else this
}This would at least solve the first problem. I get that adding the That's why I suggest that the
Given: error object ParseError
error object MappingError
error object ConnectionError
typealias HandlerError = ParseError | MappingError | ConnectionErrorAllowed: typealias NoParseError = Error \ ParseError
typealias NoParseOrMappingError = NoParseError \ MappingError
typealias NoHandlerError = Error \ HandlerErrorNot allowed: typealias DataError = HandlerError \ ConnectionError // use ParseError | MappingError instead
typealias DataError = Error \ NoParseOrMappingError // no double negatives allowed. Again, use ParseError | MappingError instead
typealias MappingAlias = NoParseError \ NoParseOrMappingError // again, don't use double negatives. This is just MappingErrorAnother situation where this would be in case |
|
Maybe I missed it, but would something like this be supported under the proposal? fun List<V>.add(value: V): Unit | OutOfBoundsI guess since To be useful, this would require that the |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
This discussion is dedicated to the new Rich Errors proposal. The current full text of the proposal is available here.
This proposal focuses primarily on design details. The previous proposal, which covers the motivation behind the feature, is available here.
All reactions