5 ms·
>One is non-locality of exceptions. Even gotos but bounded by a function. Exceptions, however...those can take you anywhere. I've never understood this argumen
by Arnavion 10y ago
>One is non-locality of exceptions. Even gotos but bounded by a function. Exceptions, however...those can take you anywhere.
I've never understood this argument.
Where does `try!()` take you if there's an error? To the caller. If the caller also has a `try!()`, where does that take you? To the caller's caller.
Where does `throw new FooException()` take you? To the caller. If the caller doesn't have a `catch(FooException)`, where does that take you? To the caller's caller.
Edit: Note to commenters: I'm not arguing about explicit vs implicit returns. I do agree that explicit returns are nicer to read and reduce the surface area of a language. My comment is solely for the "Exceptions can take you anywhere" sentiment.
- Manishearth 10y agoExplicit vs implicit basically. You don't get to ignore the error. If the caller does have a try, it propagates. If the caller doesn't have a catch, it propagates. This becomes important because now your code no longer has invisible escape hatches 20 levels deep. You design your function with specific points where it may stop executing. You control when your function returns. You can also store the error value and handle it later (Servo makes use of this pattern often -- e.g. a network request with a body of Err, which needs to be handled when we try to read the body). > Where does `try!()` take you if there's an error? To the caller. If the caller also has a `try!()`, where does that take you? To the caller's caller. You can say this about return, too. returns and try both take you to the caller. The caller then bubbles you up, because it explicitly returns or tries.
- orthecreedence 10y agoI LOVE this about Rust. Whenever I'm using another library, if a function I'm calling returns a Result, I know that I need to handle an error condition. With exceptions, you never really know if the code you're calling throws exceptions or not. The explicitness of "yes, this function may return an error condition" is really nice. And if you really don't give a damn about errors, you can just `.unwrap()` everything.
- solidsnack9000 10y agoI think this is the most meaningful and valuable thing about it: `Result` gives you a type-level indication of the need for error handling, but in a manner that is sufficiently polymorphic and friendly to handle that it doesn't end up like Java's checked exceptions (which could be seen as a special sub-system of the type system).
- pjmlp 10y agoThe problem with checked exceptions in Java is that many don't design them properly, not everything needs to extend Exception, there is a reason why RuntimeException is there since the early days. Yes, even the JDK is to blame for misusing checked exceptions.
- rvense 10y agoYou can smash ahead with unwrap()s and get something that works. And then when you want to make it reliable, you know where to look. It's like the language forces me to put in those "// TODO"s my code would otherwise be full of..
- tmzt 10y agoExcept it's treated as a normal return and could be written that way. There is no stack magic happening. The return value is a Result, which is an enum and must be handled in a match or other construct, or explicitly ignored with an .ok or .expect (that may be for Option only) which result in a panic if not successful.
- rads 10y agoThe argument only applies to unchecked exceptions because those can happen from anywhere without being mentioned by the calling code. It's easy to sneak through a layer of abstraction since the bubbling is implicit. The `try!()` in this case is analogous to checked exceptions. It makes the bubbling explicit.
- yazaddaruvala 10y ago"Checked Exceptions" and the Result pattern can be translated back and forth by a machine (i.e. syntax sugar for each other). The trick is "Exceptions" are more than just "Checked Exceptions"
- Retra 10y agoChecked exceptions probably act more like ad-hoc enums since you've got to handle each kind of exception that can be thrown, but I'm sure that could be encoded in nested results too.
- yazaddaruvala 10y agoFair enough, CheckedExceptions are almost always "fat pointers". Meaning they are all the same size, and can be passed around equivalently. The Rust equivalent would be something like: `Result<_, Box<Error>>`
- gpderetta 10y agoAmen, they have the same expressive power. The thing is that exceptions do not require the manual CPS transform required to handle Result objects, which results in nicer code. Even with syntactic sugar like do notation, the syntax is still different from 'normal' code. Also the escape hatch to unchecked exceptions is convenient too (then again Rust has some form of it already).
- paulddraper 10y agoExcept that try!() returns to the call site. Raising an exception does not. It's akin to a `return` and `goto` all in one. --- I don't mean to criticize Python's choice. I think exceptions are a good decision for a high-level, dynamically typed, scripting language. I do not think exceptions are a good decision for statically typed systems language.
- Retra 10y agoYeah there's a good number of people who just don't want exception handling and stack unwinding code in their binaries. If Rust is going to be a usable systems language, it at least needs to placate the needs of systems developers.
- bluejekyll 10y agoThere are other good comments here, but the use case where I find it most important is in lambdas. What do you do when a lambda throws an exception in the processing loop of an iterator? some_vec.iter().filter_map(|i| i.to_something() .map_err(|e|None) .map(|i|Some(i))) .collect() I think that will compile. The `to_something()` might return an error, the important thing is that there is no exception and allows for clean error handling. In this case we're just ignoring errors. With exception handling, while it's not impossible to do the exact same thing, I believe this is cleaner (and yes, there are other ways to write this).
- pjmlp 10y agoC#, C++, Java, F#, OCaml, Haskell, Lisp try_block some_vec.iter().filter_map(|i| i.to_something() .map_err(|e|None) .map(|i|Some(i))) .collect() catch_exception
- bluejekyll 10y agoThis is not correct. The to_something() is what throws the exception. In those languages, you need to put the try block inside the iterator's map lambda. Again, I didn't say it was not possible in the other languages, I only stated that I didn't think it ends up being as clean (specifically in regards to C++, Java and C#)
- gpderetta 10y agoIn most languages, it is easy to write a 'try(expr, fail)' HOF that gives you the same functionality (but possibly with different performance profiles, depending on the underlying exception handling implementation). On the other hand without non-local error handling it is harder to bail out of the outer map on a to_something error, if that's what's desired.
- junke 10y agoIn Lisp the "catch" part could be "handler-bind", which could invoke a restart (skip element, or use-value). This requires the called function to establish a restart, but the good news is that it removes the burden of the decision from the innermost part.
- zphds 10y agotry!() is a macro. Hence all it does is take a given expression and expand the whole thing into a if-error-then-return block. I guess you say that it's an explicit return under the hood.
- friendzis 10y agoIn my view non-locality manifests with wrappers when you call wrapper function, which in turn calls wrappee (how do you call it?), which throws/excepts/whatevers but the wrapper does not catch it or merely logs and rethrows it: now you get return from wrappee directly to the caller. Or in other words: "Exceptions can take you anywhere". This is a huge problem with wrappers in general that signature of wrappee becomes part of wrapper signature. Since exceptions are part of function signature they make the problem worse. And since most if not all languages do not enforce exceptions in signature there is literally no way to know what will be thrown if runtime polymorphism is used. Which results in situations where querying REST endpoint written in a language we all love to hate returns trace in the body, ooops :)
- junke 10y agoIn those cases I take a defensive approach. There will be exceptions I don't know about, and that's fine. I let go the desire to fully know the set of all exceptions that can be thrown. Instead, I weaken my assumptions and code as if everything could break, and that makes code quite robust. For example, I try to arrange the code so that it is transactional, which can be easy if the approach is functional. Otherwise, places that must be protected get a catch-all handler, etc.