Hacker News new | ask | show | jobs
by masklinn 3349 days ago
> 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.

2 comments

> 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.

> If you only have one of TryParse or Parse you cannot judge the intention.

Sure you can. To take Java as an example, if you only have one parse method and it returns an `Optional`, you can indicate the intention by whether or not you call `get` directly or call something like `isPresent` or `orElse` first/instead. Yes, you can get that wrong, but you can get the choice between `TryParse` and `Parse` just as wrong.

That's a very good point. I do this all the time with nullable types; I'm not sure why I didn't consider it that way.
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.