Instead, the trend was to avoid using checked exceptions at all, which perpetuated the crazy situation we're in where library code can suddenly abort and the author shrugs and says "shoulda read the docs".
Instead, the trend was to avoid using checked exceptions at all, which perpetuated the crazy situation we're in where library code can suddenly abort and the author shrugs and says "shoulda read the docs".
> Instead, the trend was to avoid using checked exceptions at all
Java's type system was far too weak for that to be at all practical. Early Java not only didn't have first-class functions, it didn't even have generics. Even today checked exceptions are horribly cumbersome - for example, it's simply not possible to write a wrapper function that accepts a function and calls it, and throws the same set of checked exceptions that the inner function does.
If Java had had proper ML-style types from day 1, like Rust does, then it could have done "errors are values" in a practical way. But forcing people to use checked exceptions would have resulted in either C-style errno codes, or the whole language collapsing under its own weight.
It is possible, and straightforward, to do that if the set of checked exceptions is of a statically known size, and the wrapper function can propagate the exceptions rather than catch and rethrow them. In my experience, that is a very common situation (although not universal).
If you want to catch and rethrow, that can also be done, but it requires an evil hack, and the wrapper will need to take a reified type for the exception.
My point is less that Java could have done this differently with the other type choices that they made and more that it's a shame that Java's implementation of checked exceptions has poisoned the concept for so many. I think a language with strong type inference and a checked exception mechanism would be better than Rust's Result type because you get the stack trace.
In see no difference. Except for the fact that you don't have the stacktrace (in Go), and unroll the Stack manually. In any case, there'll be a generic catch (whatever you call it) on the top level of your daemon to recover from the failed request and handle the next.
Unrecoverable errors are things like stack overflows or out of bounds array access. There is no reasonable way to soldier on after this, so the program should just end. Trying to continue the program in such situations only leads to pain. Like array accesses out of bounds that allow you to read unrelated memory.
But it’s still an evolving area. For example, failure to allocate memory - is that recoverable or unrecoverable? Initially it was thought that it was unrecoverable, and programs would panic if memory failed to allocate. This seemed reasonable, until folks tried to use Rust within the Linux kernel. Within the kernel, failure to allocate memory is recoverable. Rust is evolving the semantics here.
All this to say, yes, Rust does allow you to define functions that either fail in a recoverable way, in which case the calling function should handle it. Or they fail in an unrecoverable way in which case there’s nothing the calling function can do to recover. Thankfully, panics in third party code are relatively rare so this doesn’t happen in practice.
No, I wholeheartedly disagree with this. It's the equivalent of exit(1) some way down the stack. Whats recoverable or not depends on the use case and is a decision to be made by the caller of a function, not the implementor.
Otherwise I agree with you, that library code should not fail/crash/exit(1) just because of some judgement about recoverability, and out to clean up after itself before passing control back to the caller. If the user wants to fix some ENOSPC deep in my library by shelling out to "rm -rf /" and then trying again, that's fine by me, and this should be reflected in the API.
Do you mean that e.g. an out-of-bounds error will panic? If that's the case, you can always access arrays/slices with some checked access, that will return a Result/Option and cannot panic. But it would be a PITA if you couldn't skip that.
Usually your HTTP framework will already have this implemented, i.e. a panic in a request handler will be "caught", converted to some 500 response, and should not affect other requests.
Other points of "catch all errors" don't need that. And then there are a lot of places where you do handle errors, if they are conceptually a Result. I know you can just catch SpecificError, but the ergonomics are just horrible in terms of control flow.
> there are a lot of places where you do handle errors, if they are conceptually a Result
Yet, what's conceptually a result lies in the eye of the beholder, and should not be dictated by an API designer IMHO. Rust's ? is a step in the right direction, but I'd argue since you care about specific errors in maybe 0.1% of invocations tops (in production code), and that's a stretch, the ? should actually work the other way round. And if the mechanism does not provide a way to select specific errors (such as a proper catch clause) the errors it exposes as a result should include runtime errors as well.
- You say there is no difference between unchecked exceptions and Go's errors
- I say yes there is since Go forces users to handle errors explicitly
- You say that's not technically true in all cases.
OK. Yes. I should have said "nudges users" instead of force. It's a shortcoming of the language. It is still really hard for me to see unchecked exceptions and value-based error handling as the same thing. One of them encourages doing nothing and hoping that bubbling up is the right answer. Very often, especially in a multi-threaded context, it is not.Where do I say that? I say that Go programmers, in 99.9% of cases, do manually what exceptions do automatically. In terms of cumbersome, error values are the equivalent of checked exceptions. The equivalent of runtime exceptions are panics.
> Go programmers, in 99.9% of cases, do manually what exceptions do automatically
Our experience working with Go must be very different.
Grepping for "err := " and looking at the first 10 results in my team's codebase.
* 4 cases where the error is just returned
* 1 case where the error is returned only if it matches a certain type (otherwise logged)
* 1 case where the error is logged as a warning.
* 1 case where the error is logged, some metric is incremented, and then execution continues as usual. (a fail-open authentication check)
* 1 case where the error is returned as different error type.
* 1 case where the error is returned, but only after accessing the result (this is a strange design / antipattern), and annotating it with a human-readable explanation.
* 1 case where the error is treated as a boolean condition, and is not returned. (the error condition is "does not exist").
So in this sample it matches the "automatic behavior" in only about half the cases. In other cases, substituting the existing behavior with exception's automatic behavior would cause severe bugs.(Obviously, without source code access the following is guesswork) The other cases might either not be required (since you'll get a stacktrace anyway and don't need to leave breadcrumbs) or be part of some general infrastructure, say interceptor, that logs interesting things on boundaries. At least that's my day to day experience comparing Go and Java.
Actually go runs perfectly fine if you just do nothing with the error, and then you are flying past errors like it is nobody's business. Of course you can you static analysis to tell you if you forgot to handle an error, and most people do, but that is not really a language feature.
In Java a programmer is also forced to make a decision to add explicit exception handling or not. But if they choose to not add it, the program won't just pretend like all is fine and dandy, it will stop the execution of the current function and pass the exception up the stack.
As for stack traces: checked exceptions are semantically almost identical to error-as-value but they do give you a stack trace. They give you all the static guarantees of error-as-value plus the benefit of being able to know where the error came from.
I completely agree with you on that one. The only thing I'd want to add is that aside from example code, usually you don't care a lot about what specific errors might pop up, unless you're at the top level of your program. This might be the request handler, or some UI loop. On the other hand, where you care about errors, you usually don't care about results or their types.
I've been in the industry long enough that I vividly remember the catch-and-wrap orgies in Javaland of 2003. A lot of it was caused by the inferior type system of Java, which hinders composability (such as when a layer is moved to another node and suddendly you have to deal with remoting exceptions). But it also goes to show that knowing a specific class of error is often overrated in application code, and should be, well, the exception.
The only thing that I'd add to the unchecked exceptions is noexcept with compile-time checking. Something along the lines:
1. You can declare method as `nothrows`. Compiler will ensure that no exceptions are thrown (`java.lang.Error` can still be thrown, but you're not supposed to deal with it in any way in most code).
2. You can declare method as `throws Exception1, Exception2`. Compiler will ensure that only subclasses of those exceptions are thrown from this method.
3. You can omit any declaration. In this case compiler will compute list of possible exceptions implicitly (it's a union of all exceptions thrown by all called methods).
So basically it'll allow to: document list of thrown exceptions and it'll allow to statically check that other exceptions are not thrown, so this documentation is compiler-checked.
Of course this idea needs battle testing, but I think that I'd like it. You can either opt-in and write code documenting all thrown exceptions (which is good for libraries) or you can opt-out and write simple code without bothering with exceptions (which is good for applications).
Also there should be proper support for generic exceptions, so I can write Function<T, R, E> and E would be of type containing union of all thrown exceptions. That's required for example for moving exception signatures from lambdas in functional collections.
And even though C++ dropped exception specifications, they still kept the difference between might throw anything or doesn't throw at all, and there is also the paper to reintroduce them Swift style.
Recoverable error states are part of your function's interface. Languages with exceptions make this implicit: in order to know how to interface with a function's error states you have to go digging through the docs, which hopefully document the exceptions it throws. If they don't, then you either have to trace the entire call tree or just wait for the library to throw something so you know what you're supposed to be catching.
On the other hand, if a language requires that recoverable errors be documented in the function signature (either as a checked exception or error-as-value), you know that you're either handling the exceptions or intentionally letting them propagate. There are no surprises at runtime because the set of all possible error states is known statically.