Hacker News new | ask | show | jobs
by masklinn 3344 days ago
> + no checked exceptions (checked exceptions have benefits in theory but real-world practice shows that it forces programmers to copy-paste mindless boilerplate to satisfy the checked-constraint)

As languages like Rust or Swift demonstrate, the issue is less the checked exceptions and more the abject lack of support for them in the language and type system, which ends up making them essentially unusable.

> Gosling said that unsigned types are confusing and dangerous

He is right of course, but he forgets that so are signed types if they're bounded.

If Java had unbounded signed integers (à la Python or Erlang) that'd be one thing, but Java does not, and neither does it have Pascal-style restrictions/user-defined integral bounds, which means it's no less confusing or dangerous, it's just more limited.

2 comments

Neither Swift nor Rust have exceptions, checked or otherwise.

The kind of exceptions that unwind the stack until some part of the code up the stack catches the exception.

They both handle errors by returning error values, kind of like Go.

Rust has a try! macro, which might make you think it's try/catch equivalent, but it's not. It's just a syntactic sugar over error values.

Similarly in Swift try/catch/throw is just syntactic sugar for handling error values.

https://doc.rust-lang.org/book/error-handling.html

https://developer.apple.com/library/content/documentation/Sw...

While it's true that Swift and Rust don't unwind the stack, this is just an implementation detail.

The comparison to Go is very misleading. Neither Swift nor Rust require you to return a value on error like Go does, nor do they allow you to simply ignore errors. The semantics, not the implementation, is what matters.

Swift's error handling is not "syntax sugar." try/catch in Swift are not macros that desugar into normal Swift. Errors are not returned via the normal return path, but via a dedicated register. Just like stack-unwinding exceptions, Swift errors are part of the core ABI.

https://github.com/apple/swift/blob/master/docs/ABIStability...

I have absolutely no experience with Rust, but I know it normally depends on libunwind, isn't that for unwinding the stack?
Perhaps to implement https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

But also just to display traces on asserts/panics, even if not "unwound" per se.

By default, rust's panics _do_ unwind the stack. However, you can also set a flag to compile them as an abort instead. Stack traces are still useful in that case.
> Neither Swift nor Rust have exceptions, checked or otherwise.

The point is that both of them have generic error types which "infect" every caller (transitively) in much the way checked exceptions do.

That Rust and Swift require explicitly bubbling error values should be a point in favour of checked exceptions.

> That Rust and Swift require explicitly bubbling error values should be a point in favour of checked exceptions

I thought the explicit bubbling was the nicest thing about error handling in Rust. It's usually just a single character ('?') that the editor can easily highlight, and nicely indicates where the operations are that might fail.

I'd add that checked exceptions don't play nicely with general functional/stream operations like "map" which is why Java went with unchecked exceptions for their streams api in Java 8. Rust on the other hand can handle interior failures in such functions nicely, promoting them to a single overall failure easily via collect(), using the blanket FromIterator<Result<A, E>> implementation for Result<V, E>.

> I'd add that checked exceptions don't play nicely with general functional/stream operations like "map" which is why Java went with unchecked exceptions for their streams api in Java 8. Rust on the other hand can handle interior failures in such functions nicely, promoting them to a single overall failure easily via collect(), using the blanket FromIterator<Result<A, E>> implementation for Result<V, E>.

And Swift has a `rethrows` marker to transitively do whatever a callback does, it's equivalent to "throws" if the callback throws, and to nothing if it does not. So e.g. `map(_ fn: (A) throws -> B) rethrows` will throw if the callback it is provided throws, and not throw if the callback doesn't throw.

As I see it, the Rust (etc) approach avoids two problems:

1. The C problem of forgetting to check error returns. Yes, exceptions (checked or not) also avoid this.

2. The C++/Java/C# problem of exceptions being more expensive than you'd like for common error situations. .NET has sprouted alternatives like `bool tryParse(String, out int)` as a workaround, but on balance I prefer the unified mechanism.

What I don't like is how innocuous `unwrap` looks, or how often it appears in example code.

> The C++/Java/C# problem of exceptions being more expensive than you'd like for common error situations.

Exceptions should not be used for common error situations! This is the prime mistake of Java which pretty much required exceptions when it should (and checked exceptions to boot).

> .NET has sprouted alternatives like `bool tryParse(String, out int)` as a workaround

I don't consider that a workaround. There is a deep semantic difference between TryParse and Parse. If you see TryParse then you know the data is expected to be invalid. If you see Parse than you know it's expected to be always valid. A good C# program should have very very few try/catch blocks (ideally just one).

> There is a deep semantic difference between TryParse and Parse

Sure, but I don't think that splitting every possibly-failing API call into throwing and non-throwing forms is the right way to express that difference, and a lot of the time it'll be a matter of context (meaning that the implementation can't magically do the right thing).

It's fairly easy to layer throwing behaviour on top of nonthrowing in a generic and efficient way (Rust's Option, Java's Optional etc), but the reverse is not true.

I must admit I'm losing track of what if anything we're in disagreement about, though...

I agree it's easy to layer throwing behavior on top of non-throwing behavior -- Java easily chose the worst possible way to do it.

But having both a Parse and TryParse means that I can ignore the result of the Parse call entirely and let it fall through to the exception handler. It is by-definition always expected to succeed so when it doesn't then that's a bug. If you only have one of TryParse or Parse you cannot judge the intention.

It's not really that innocuous. It'll cause the program to crash as soon as it's discovered that there wasn't a value where you were expecting one.

In languages with exceptions, the program will crash as soon as you try to use that value (rather than when you try to unwrap it), e.g. the infamous null pointer exception. Copying bad sample code in this case might result in code that is difficult to debug because a null value might be handed off several times before something tries to dereference it.

In languages that expect but do not enforce that you check the validity of the value (like C) you'll just get undefined behaviour that will hopefully cause your program to segfault when you try to use the value, but who knows what will actually happen? Copying bad sample code in this case will cause a security vulnerability.

Copying "bad" sample rust code (using unwrap) will cause a safe crash with maximum locality, for simpler debugging.

> The kind of exceptions that unwind the stack until some part of the code up the stack catches the exception.

Rust panics result in unwinding the stack:

https://doc.rust-lang.org/nomicon/unwinding.html

try!/? is preferred for handling errors, but if you "know" that a Result or an Option has a value, you can unwrap() or expect() it, and you'll get a panic if you're wrong.

so does panic() in Go. But no-one claims that Go uses exceptions for error handling, because it doesn't and neither does Rust.

In both languages panic() is for "this shouldn't happen" fatal errors, not for signaling errors to the caller, the way Java or C# use exceptions.

My comment was in response to person basically saying "checked exceptions in Java would be good if only they implemented it the way Swift/Rust did".

With Swift, that assumes that the function being called reports the error in the first place. Very few functions do.

Cast a large double to an int -- crash. How do you catch that error? You can't. You have to make sure it never happens yourself. The UserDefaults is another great one. All sorts of ways that it can crash your app, none of them can be caught or handled. Your app just crashes. My advice: convert all your object to a text string (like json) and store that. Do NOT store a dictionary representation.

FWIW the try!() macro has been replace with the ? operator.

Having spent extensive time with checked and unchecked exceptions I find Rust's error model to be very robust.

There's extensive tooling to handle and transform them, most of the pain comes from developer who are used to sweeping them under the rug(which totally makes sense in prototype land but when I'm shipping something I want guarantees).

I find unsigned types more dangerous than signed bounded types. For unsigned types the edge-case is at 0, a frequently used number in eg. arrays. For signed types the edge-cases are at (random) positive and negative numbers.

Having to think more about edge-cases makes the code more dangerous:

    for(uint i = arr.len()-1; i >= 0; i--) {...}
> I find unsigned types more dangerous than signed bounded types.

So you do agree that signed bounded types are dangerous, and it's only a matter of degrees between them and unsigned. Thank you.