Comments (5)
A a;
try {
a = someThrowingFn();
} catch (AException ex) {
throw new RuntimeException(ex);
}
It sucks. In Swift this is simply: val a = try! someThrowingFn(); Kotlin is moving towards having both unchecked exceptions (panics) and errors as values. I think its absolutely the correct decision. They're also including an easy way to panic when you can't handle an error: val a = someThrowingFn()!!;The other thing that I think made checked exceptions worse in the beginning was the lack of pattern matching and sealing with exceptions. Being able to exhaust all the different types of a particular error is really important if you want to properly respond. If you had a meaningless top level exception like IOException you couldn't effectively determine what actually happened and everyone just defaulted to unchecking it. Nowadays it's a little better if you seal your exception hierarchy:
sealed abstract class SomeException extends Exception permits A, B {
final class B extends SomeException {}
final class C extends SomeException {}
}
try {
someThrowingFn();
} catch (SomeException ex) {
switch (ex) {
case A a -> iCanHandleA(a);
case B b -> throw new IllegalStateException(b); // can't handle this one
}
}
Hopefully that becomes even better with the exceptions in switch [1] proposal: switch (someThrowingFn()) {
case throws A a -> ..
case throws B b -> ..
}
The real big issue like you said is that they're not completely in the type system. Especially when it comes to lambdas; you need to invent a Result<T, E> type every time you want to work with lambdas. Obviously all of these things are solvable with library code, try! is easily some static panic function; it sure would be nice to not have to write them. Overall I'm hopeful that with the overall programming community moving back towards checked errors that the OpenJdk team will invest in making checked exceptions better, but only time will tell.[0] github.com/Kotlin/KEEP/blob/main/proposals/KEEP-0441-rich-errors-motivation.md [1] https://openjdk.org/jeps/8323658
I also used the C++ exception specifiers, which people keep forgetting is where Java's idea came from, alongside other languages like Modula-3.
The legendary only feature so bad it was actually removed from C++? Those exception specifiers?
If that is the point your trying to make.
I on the contrary, would rather use the full language without dialects.
And the feature worked, unlike export templates or C++ GC, also removed.
The kind of structure mentioned in the article, with things like contexts, and calling chains, is stuff that I did for years.
In my experience, it had a glass jaw. It broke easily. The more complicated something is, the more places it has to break.
Also, I found that I never, ever used it. The most introspection I ever did, was look at an error code. Where the error happened, was the important thing, and a symbolic debugger gave me that.
These days, I just declare an Error-conformant enum, sometimes with associated data, and add a few computed properties or functions.
It’s most important that I write my code to anticipate and catch errors. The report is always refined to be as simple and gentle to the user, as possible. The technical stuff, I have found, is best handled with a symbolic debugger.
But that’s just me, and I know that the type of software that I write, is best served with that model. YMMV.
In Java common errors and exceptions, like file I/O errors, have to be declared on each function signature, except everything under the RunTimeException hierachy is exempt from this requirment. In the language, RunTimeExceptions these are the messy errors like NullPointerException and ArithmeticException.
In practice, people do subclass program-specific exceptions under RunTimeException in Java, as well as wrapping existing exceptions inside a RunTimeException, for the sole purpose of not having to add them to function signatures all over the place.
You are correct, but the logic is inverted. Most anticipated errors in Rusts are handled by Result. Errors that aren't anticipated i.e. panics, crash the thread, and if it's the main thread the program.
In Java terms, Rust has return value as error, which can be thought as checked exceptions; and Error class in Java that just kills your program.
Stuff isn't as clean cut ofc because Rust has allowed panics to be caught by main thread.
I thought it was because you couldn't be fully generic over exceptions.
Have also been picking up Java and the thing that comes to my mind is sealed classes, and whether they are similarly bad design.
Before Swift 6 a function either could throw nothing or could throw anything, like Java’s “throws Exception”.
Now you can mark that a function can only throw a specific error type but nothing else, like Java’s “throws MyCustomException”.
Seems you can’t list multiple types. You’d have to have them in a class hierarchy and mark the base type.
What I like about this idea: if a function is marked like this, Swift can tell that a catch block is exhaustive without needing a “catch all” error case. And if the function starts throwing more possibilities than you coded for, e.g. new case added to an enum, you get a compiler error! Not a submarine bug.
… but Swift has also the tooling to declare ABI-stable enums without freezing them. Clients are forced to handle unknown cases.
I’m not sure how this would interact with the API compatibility Swift has. Maybe that would help authors.
Since you can only put one name between those parenthesis, the only way to do all three would be to have a base class and list that. Or at least that’s my understanding.
However if you do that and there’s a fourth type called UnwantedError, that’s also valid to throw even though you don’t want to.
It’s an interesting limit. I didn’t follow this feature, though I remember hearing about it, so I don’t know why that choose was made.
I think the Swift way would be
enum FooError: Error {
case myError(MyError)
case badError(BadError)
case thirdError(ThirdError)
}
and declare your function as throws(FooError). That forces handlers of those exceptions to consider all cases.Edit: I think there’s a case for having the compiler synthesize that if one writes something like throws(MyError||BadError||ThirdError), but I’m not sure I’d would like to have such magic. Would two functions with such a declaration throw the same type of exception? That might matter if you do stats on exceptions logged, for example)
You can also declare errors as structs, so that’s where you could use inheritance.
Sealed classes fall into the realm of things that the Swift language designers would leave up to convention. For example, using underscored public methods for internal methods that need to have external linkage, rather than inventing a new keyword for this purpose.
Java's checked exception issue is probably another problem altogether. I'd tend to say control flow issue perhaps?
When you have a typed throw that throws different errors for every edge case failing, even when an edge case failing is part of the expected behavior of the system, makes it much easier to reason about. Even when there is nothing happening towards the user or being logged.
Example:
Restore a user's session. It can fail because:
- Nothing was stored
- Something was stored, but it was in an outdated format
- Something was stored, but the token is expired
- Etcetera
It doesn't matter to the user, it's expected behavior, but at the same time they're different branches that all need to be covered.
`static func validate(name: String) throws(ValidationError)`
Would be handled as:
```
do {
try UsernameValidator.validate(name: name)
} catch { switch error {
case .emptyName:
print("You've submitted an empty name!")
case .nameTooShort(let nameLength):
print("The submitted name is too short!")
}
}```
extension String: Error { }
Because I like doing this: throw “Something went wrong”
Admittedly I don’t use this in shipping software.As for the feature: My “hot take” is: I’m skeptical this will really make software better and I suspect instead programmers will just spend more time designing error types and error handling than just creating software.