Replies: 4 comments 19 replies
-
This is appealing, but concretely how would you suggest implementing this in Base? We cannot change functions to return |
Beta Was this translation helpful? Give feedback.
-
Some general remarks on this topic: Two kinds of error is not a problemThe error handling system proposed here is quite simple. In fact, it is so simple that there is barely any system that needs to be learned: It is true that the proposal will create a choice for the programmer as to whether a given function should throw or return error values. However, programmers already have to make the conceptual distinction between recoverable and unrecoverable errors when writing code, no matter the language. In my experience, the choice is not too hard, and in the rare cases the user wants both, it's trivially easy to implement a throwing version given an error-returning version. To prove the point: Is anyone confused by the fact that Differences between Base's approach and this proposalOkay, so e.g. First, this proposal allows arbitrary error types. I think it's obvious that this is preferrable - after all, some times there is actual error value that needs to be considered. We can always later add better ergonomics for Second, Third, there is the issue of safety: Safety versis convenienceOne major advantage of On the other hand, the very fact that the user is able to ignore possible errors also makes it bug-prone, which leads to unstable code in the wild. Let's be real: Users often don't remember edge cases, or even understand how foreign APIs can fail. Having your code return the expected So we have a direct conflict: Wrapping Can we have the best of both worlds? The only possibility I can think of is to implement both for a large set of failable
Having two methods for many of Having both versions would enable prototyping and exploring while simply ignoring the possibility of errors, like we can do now, while also being able to transition the existing prototype code to more safe, well behaved code. This plays into Julia's existing and well-known strength of being a language good for both prototyping and performance sensitive production code. In essence, I hope that on the issue of correctness, Julia could bridge the "two language problem" between error prone but expressive scripting languages and rigid static languages. Julia is already in a particularly good position to do this, since it somewhat uniquely is both dynamic and expressive, and also have an optimizing compiler that enables static analysis. Struct type vs union typeI made a list of pros and cons of the two representations of error values here: jakobnissen/ErrorTypes.jl#5 Where to go from here?Here is a possible plan of action to implement this in
TL;DR
|
Beta Was this translation helpful? Give feedback.
-
One thing to keep in mind is that we used to have
I don't think we can emulate that API in Julia without unacceptable performance penalty. Go's approach is rather interesting since
is already something "normal" in Julia. |
Beta Was this translation helpful? Give feedback.
-
I opened a concrete RFC #45080 |
Beta Was this translation helpful? Give feedback.
-
The current error handling landscape of Julia is very far from how good it can be. On one hand, we have
throw
andtry
-catch
at the language level but the compiler does not optimize it well. On the other hand, there are various packages 1 that provide a framework for reporting errors by returning a special type (like Rust'sResult
and Haskell'sEither
). However, doing this in an external package has the downside that we can't provide efficient error handling for methods defined inBase
that require touching the internals.The main question is: Can we have this "throw"-by-returning convention in
Base
?More specifically, can we have a minimal set of types such as
and minimal APIs around them in
Base
? For example, it let us implement an API liketryget(dict::Dict{K,V}, key) -> result::Union{Ok{V},Err{KeyError}}
.If we have minimal types for result value convention in
Base
that is used across the entire ecosystem, people can independently experiment with tooling (e.g., macros and functions) around it. Once these toolings have matured, we can consider integrating it inBase
or in the language.Concern: two modes of error handlings
Clearly, the main concern with this approach is that having two modes of error handling is rather ugly. It is reasonable to argue that it would be more beneficial to improve the error handling in a way more integrated to current exception handling (e.g., julep: "chain of custody" error handling) and improve the compiler to optimize the error handling. However, it may be beneficial to have two modes of error handling clearly distinguished by the entity to which the error is reported. The idea is to separate the kinds of errors into two cases:
Unrecoverable error: This is an error due to a mistake made by the programmer. It is unrecoverable by the caller program of the API since the invariance expected inside the callee API has been violated. It should be reported to the human programmer.
Recoverable error: This is an error that is clearly documented that it can happen. The caller may choose to handle this situation; i.e., it should be reported to the caller program.
The requirements for the mechanism of these kinds of errors are different. Since an unrecoverable error (unsatisfied precondition, violation of internal invariance, etc.) is a bug, it is important to record as much information as possible for reporting it. The performance impact is negligible because the program is likely to wait for human intervention after the error happens or invoke an expensive error logging routine. Thus,
throw
ing an exception for this is adequate. Since handling this type of error typically happens at the level of UI applications such as REPL and IDE, thecatch
-all syntax we currently have is consistent with this notion of error.On the other hand, the recoverable error needs to be as efficient as possible since it can occur in tight loops where the erroneous situation is somewhat expected (e.g., singular matrix detected in a linear solve with a small static matrix; contention detected in a nonblocking algorithm). Given excellent support of small
Union
in the compiler, it seems that the "throw"-by-returning convention suggested above is a good candidate for this kind of error handling. Furthermore, since the caller needs to know what errors are possible, the possible set of errors is captured in the API description. It simplifies our practice in API specification since we can simply document what is returned and not what can bethrow
n.Multiple error handlings in other languages
It is not uncommon to have two modes of error handling. For example, Rust has panic for the (mostly) unrecoverable mode of error handling. This can occur, for example, when there is a bug when the programmer used
Result::unwarp
to unwrap a value that is assumed to be anOk
while in fact there is an error in the logic. A similar mode of error reporting of unrecoverable logic errors has been suggested for C++ 2 as well.Go also has panic besides its
result, err = Api(...)
return value convention. Like Rust, it can be used for unrecoverable errors. But Go's panic is subtlety different from Rust in that it could be used as a recoverable/expected error within a package. However, the error reporting across API boundaries does not typically use panic. Thus, it's another example where the error handling at API boundary is distinct from unrecoverable and internal errors.These are some relevant examples that I remember but there are likely more examples like them. It's also sometimes unclear what it means to have "multiple modes." For example, Java and Python use exception type hierarchy to distinguish unrecoverable and recoverable errors. But they are more uniformly handled at the syntax level.
I don't want to say "it's Ok since others do it." But, just as a sanity check, it is good to know that having multiple modes of error handling is an accepted solution in other languages.
Other properties and implementation strategies of
Ok
/Err
approachI discussed other upsides and the implementation approaches of the "
Ok
/Err
approach" in https://github.com/tkf/Try.jl such asUnion
or astruct
? Or a hybrid?Err
Julia needs a fresh take on error handling
Julia provides the performance of static language with a highly interactive approach to programming. This human-in-the-loop approach to programming requires a careful design in exception handling and it explains why it takes time to arrive at a good solution. However, now that Julia is in 1.x phase, I think it is time to provide some working solution. I think the approach based on
Union{Ok,Err}
supports both the performance requirement and high-level programming style of Julia.Footnotes
There are packages such as jakobnissen/ErrorTypes.jl, iamed2/ResultTypes.jl, KristofferC/Expect.jl, and tkf/Try.jl. There are recent discussion in [ANN] ErrorTypes.jl - Rust-like safe errors in Julia - Package Announcements / Package announcements - JuliaLang and Try.jl - JuliaLang - Zulip. ↩
See "4.2 Proposed cleanup: Don’t report logic errors using exceptions" in Zero-overhead deterministic exceptions: Throwing values (PDF) or CppCon 2019 talk (44:45). It is based on Joe Duffy - The Error Model. ↩
Beta Was this translation helpful? Give feedback.
All reactions