6 ms·
This article is unconvincing. From the article: Compared to the first snippet, this one is harder to debug. It seems so because 1.) You don't know that callMe
by flowerlad 6y ago
This article is unconvincing.
From the article: Compared to the first snippet, this one is harder to debug. It seems so because 1.) You don't know that callMe will throw an error or not. 2.) If it throws an exception you will not know what is the type of the exception. The more modular your code is, the harder it is to debug exceptions.
This is only a problem with unchecked exceptions as seen in TypeScript and C#. Checked exceptions are the solution.
The problem with error code return is tediousness:
int foo() {
int result = bar();
if (result == ERROR_CODE)
return result;
int result2 = baz();
if (result2 == ERROR_CODE)
return result2;
int result3 = bazz();
if (result3 == ERROR_CODE)
return result3;
}
Compare to the same code written in a language that supports checked exceptions:
int foo() throws SomeException {
int result = bar();
int result2 = baz();
int result3 = bazz();
}
In the first version the logic is interspersed with error handling, which makes the logic hard to see.
How do people using Go and other languages that don't have exceptions solve this reduced readability problem?
- steveklabnik 6y agoIn Rust, your code would look like this: fn foo() -> Result<i32> { let result = bar()?; let result2 = baz()?; let result3 = bazz()?; } (This wouldn't compile because you don't actually return anything in the success case, but I just kept the code the same. Additionally, I am assuming you are using some sort of crate that helps with error type mapping, because that's realistic, otherwise, it would be Result<i32, Error> where Error would be some sort of type mapping to SomeException's type.)
- oleganza 6y agoRust has an operator for "re-throwing" error: question mark. fn foo() -> Result<int, SomeError> { let result = bar()?; let result2 = baz()?; let result3 = bazz()?; Ok(result + result2 + result3) } It's still 100% more explicit about return types and errors, but has the same lightweight feel as in JS/Java/etc.
- flowerlad 6y agoIn your example if bar() returned an error your program still needlessly calls baz() correct? This is why exceptions are better.
- dilap 6y agoNope, the question mark operator is syntax sugar for the "if error, return it" dance. (Cf. https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...)
- ben-schaaf 6y agoNope, it returns an error if bar returns an error - immediately. It's essentially shorthand for `if x.is_none() return X;`
- estebank 6y agoThat code desugars to the following fn foo() -> Result<int, SomeError> { let result = match bar() { Ok(val) => val, Err(err) => return Err(err.into()), }; let result2 = match baz() { Ok(val) => val, Err(err) => return Err(err.into()), }; let result3 = match bazz() { Ok(val) => val, Err(err) => return Err(err.into()), }; Ok(result + result2 + result3) }
- dilap 6y agoGo does indeed use the tedious approach. Rust and Zig have nicer syntax for it. But personally, I prefer even Go's "tedious" approach to (unchecked) exceptions, simply because it's too easy to miss an error case otherwise. I also find that knowing where errors occur is important information, so I don't really mind having it add an extra line or two. (But the more minimal Rust/Zig syntax is nicer.) (This attitude comes from having once worked on a python server application, which involved a lot, "oh shit, that throws" whackamole.)
- flowerlad 6y ago> "oh shit, that throws" whackamole You don't have that problem in languages that support checked exceptions.
- dilap 6y agoAgreed, mostly true. (It's still not as explicit about where your function might exit, but you won't get surprise exceptions, which is way nicer.)
- kibwen 6y agoYou may still have that problem if the language also supports unchecked exceptions.
- ufmace 6y agoI've never seen a language that supported checked exceptions and actually used them consistently for everything. But then that would be kind of awful, because IMO the checked exceptions cases for "I know this will never actually throw, leave me alone" and "This is just a quick script, go ahead and blow up the whole program if this fails" are poor, forcing you to write try catch blocks all over the place. IMO, Rust has a nice approach for being stricter - unwrap. If it's a quick script, throw unwrap everywhere with reckless abandon. For real programs, grep for unwrap and make sure for every one, you really know for sure that it will never fail or that blowing up the whole program if it does is the right move. To convert from one to the other, grep unwrap and actually think about what you want to happen if that line is Err.
- 6y ago
- kevinmgranger 6y agoTo add on to what other commenters have pointed out with `?`, there's also the interesting [fehler crate](https://github.com/withoutboats/fehler https://github.com/withoutboats/fehler), which lets you write code that _looks_ like it uses exceptions. It wouldn't make the examples that already use `?` any clearer, but it does reduce noise around the happy path.
- wwright 6y agoI believe that withoutboats has deprecated that, and now recommends anyhow (for applications) or thiserror (for libraries). I’ve tried them, and they really do take most of the toil out of defining errors.
- Nullabillity 6y agoPretty sure you're talking about Failure, not Fehler. Failure is (like Anyhow and Thiserror) for defining error types, Fehler is an alternate syntax for the functions that return those errors.
- thramp 6y agofehler isn't deprecated, but _I_ did deprecate the similarly named https://github.com/rust-lang-nursery/failure/ https://github.com/rust-lang-nursery/failure/ with withoutboat's blessing. failure proceeded fehler by several years.
- kibwen 6y agoThe article doesn't get into it, but Rust has an error-handling operator, `?`, that encapsulates the branch-and-return-on-error check. Your code in Rust would look like: fn foo() -> SomeResult { let result = bar()?; let result2 = baz()?; let result3 = bazz()?; } The fundamental difference between Rust's approach and Java-style checked exceptions arises when those three functions have different error types. In Java, you would annotate the function with every possible exception that the function can throw. In Rust, a library (or module within a library) would normally design its own Result type to encompass every error that the library/module can throw, and use that type on every error-producing function (you could reproduce the Java approach by defining a new Result type for each function, but I've never seen anyone bother with that). The end result is that you avoid the precise bookkeeping that so annoys people about Java's checked exceptions, but you also tend to lose some precision with reading the code: from reading the type signature you know that a function with a custom error type has the potential to error, which is useful, and you know that the error must be at least one of the types in the error's definition, but it might not exhibit all of the error types that the custom error type can contain. Think of it like a spectrum: from not having any indication that that a function may error (e.g. JavaScript), to knowing that a function may error but not having any indication of how (Swift), to knowing a function may error with at least one of a known set of types (Rust), to knowing that a function may error with an exactly known set of types (Java with checked exceptions).
- artificial 6y agoThank you for the excellent comparison with error precision. I’m diving into Swift (Vapor) and was curious if this remains true with Swift 5.2?
- kibwen 6y agoI'm no Swift expert, but I don't believe that any changes have been made to its idiomatic error-handling approach. Using the first example from the Swift 5.3 docs ( https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html https://docs.swift.org/swift-book/LanguageGuide/ErrorHandlin... ): func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws { let snackName = favoriteSnacks[person] ?? "Candy Bar" try vendingMachine.vend(itemNamed: snackName) } The `try` is analogous to Rust's `?`, and the `throws` is the part of the type signature that indicates that the function may error but without any additional information about how it may error. Of course, I imagine a capable IDE should still be able to inspect all the `try`s in a function to determine what types can possibly be thrown.
- baq 6y agoChecked exceptions are evil because they can cause transitive API breakage in cases a function or method doesn’t catch because it can’t or shouldn’t handle the condition.
- kibwen 6y agoSince this thread is also about Rust, and since I mention in another comment about how Rust libraries define their own error types, I should mention that Rust can avoid this problem by the combination of 1) adding a new way that a function can error only requires adding a new case to the library's error enum, leaving the function's type signature unchanged, 2) adding new cases to the error enum can be kept from being a breaking change via the use of the non_exhaustive attribute, which requires anyone manually handling the error to add a default case even if they handle all the other existing cases. Of course, if library consumers are just throwing a panic in that mandatory default case then you might be trading a compiler error for a runtime error, which can be controversial. The language design challenge of this decade is finding compromises between allowing libraries to change and evolve while mitigating the disruptiveness to downstream consumers.
- flowerlad 6y agoSorry that doesn't make any sense to me. If a method can't or shouldn't hand a condition it then it needs to declare itself as throwing that exception (or a wrapper around it).
- withinboredom 6y agoThey said transitive. To me, they meant if you have 3 layers of deps, A -> B -> C and C now throws something, B doesn’t catch it or mark as throws. So now you have a failure. Obvs, you’d fix this immediately, but it’s still work that you have to do when you could have been doing something productive.
- ddek 6y agoHere's Hejlsberg on why C# doesn't have checked exceptions: https://www.artima.com/intv/handcuffs.html https://www.artima.com/intv/handcuffs.html.
- higerordermap 6y ago> How do people using Go and other languages that don't have exceptions solve this reduced readability problem? We don't, we just make memes about it and move on.
- armSixtyFour 6y agoHA! That error handling was one of the more tedious parts of golang to me. Though Rust has it's own quirks, the error handling was one of the more compelling reasons for me to switch my side projects to use Rust instead of Go. That being said, Go is much more fully featured than Rust and a fantastic language.
- marcrosoft 6y agoThe problem with exceptions is they make it way too easy to not handle and error. At least with the first example you had to at least think about the error as you typed return err.
- valand 6y agoThat and `catch em all` attitude being promoted
- ImprobableTruth 6y agoThe Either monad is a very nice solution for this in my opinion, in Haskell it's just do bar baz bazz with it immediately returning after the first error value it encounters or returning the actual result (this is also how Rust solves it, though they introduce specific syntactic sugar for it rather than an universal monadic sugar).
- fiedzia 6y ago>How do people using Go and other languages that don't have exceptions solve this reduced readability problem? Rust has ? operator which converts "let x = foo()?" into "if foo returns Ok(value) assign value to x, otherwise make the function in which it is used return given error". Go doesn't care about readability.
- cle 6y agoGo cares very much about readability. It’s a very subjective topic...because someone doesn’t agree with your opinions on a subjective issue doesn’t mean they don’t care.
- dragonwriter 6y ago> How do people using Go and other languages that don't have exceptions solve this reduced readability problem? Having a single clear return path with, e.g., monadic returns and pattern matching is more readable than exceptions (particularly, the signatures are more readable than with checked exceptions effectively splitting the type system into two separate components with distinct syntaxes). OTOH Go (which has a very close equivalent of exceptions but idiomatically prefers using it's annoying multivalue return pattern in most public APIs) is pretty much the worst of all worlds.
- otabdeveloper4 6y agoYou're not supposed to catch exceptions. They are not for handling logic. They're there to make fatal errors across threads and modules be safe and robust.
- jcranmer 6y agoThere are roughly three kinds of exceptions. The first is the set of exceptions that could potentially be caused by any basic instruction that are really more of a "the programmer screwed the code up, and our static type system isn't powerful enough to prove it can't happen." Think Java's RuntimeException, the basic processor signals in C/C++ (SIGSEGV, SIGFPE, SIGILL, SIGBUS), or Rust panics. For these exceptions, the standard handling behavior is to catch it at a very high level, log as much details as you can about what caused the exception, fail the task (e.g., return HTTP 500 error), and throw away the detritus. These exceptions should be as invisible to the user as possible, and ideally you want to spend a lot of time at the point the exception is thrown collecting as much information as possible for logging purposes (such as stack traces). The second class is the exact opposite: you're doing a simple operation that is expected to fail. Such as opening a file (it might not exist) or querying a hashtable (the key might not exist). Errors here are almost always going to be handled immediately by the caller, and so collecting a lot of details about the error is not usually what you want to do. Using a simple error code for return is precisely what you want to do in this situation. The final class of exceptions is perhaps the most common: you have failures that need to be communicated from point A in the system to point B somewhere distant. My prototypical example is a (recursive-descent) parser: if the lexer sees a token it doesn't understand, you want the entire lexer and parser call stack to propagate the error to the caller of the parse function. Sometimes with these errors, you want or need to attach more contextual information as they propagate: in a SAX-like parser, you might want to explain the path to the token that failed. For the last error class, error-code handling is clearly too cumbersome for most use cases. The easy propagation of C++/Java-style exception helps, but unifying the underlying implementation with the first implementation may be unnecessarily penalizing speed. And the silent propagation turns out to be a disaster for both user maintainability and compiler optimization. That such an exception can be thrown needs to be a fundamental part of the type of the function. Java introduced checked exceptions to express this type correctly, but the way exceptions integrate with the rest of the type system has turned out to be less than satisfactory. On top of these exception classes, there are two more important considerations. The first is the general need to recognize chaining of exceptions: you may need to explain the cause of a high-level exception in terms of a specific low-level exception. The second is that you may want to convert between the different classes--turning a file-not-found exception into a hard program crash, for example. (This is especially useful when writing exploratory code!) In my opinion, none of the languages I'm familiar with have solved all of the issues with exceptions. Exceptions are hard to design right, and since they're so fundamental to how you express code, they're a feature that is baked so early in the process and is difficult or impossible to change as you discover problems with it. I do think that Rust's Result-versus-panic, with the ? syntax for Result propagation (as noted by several sibling comments) is a good starting point, but my feeling is that more work needs to be done on easing the burden of specifying exception types when you're plumbing together many stages and libraries.
- Mawr 6y ago>The problem with error code return is tediousness Tediousness is only relevant at the time of writing the code, which is the least important part of programming. You read code 100x times as often as you write it, writeability is simply the wrong aspect to optimize for. >How do people using Go and other languages that don't have exceptions solve this reduced readability problem? Readability isn't reduced, it's enhanced - the control flow is explicit, there's no magic in the form of implicit returns.
- RussianCow 6y ago> Tediousness is only relevant at the time of writing the code, which is the least important part of programming. You read code 100x times as often as you write it, writeability is simply the wrong aspect to optimize for. I'm kind of tired of this argument, as if the writability of a language doesn't matter at all. Over a long enough time frame, with a large enough team, etc, this is absolutely true, but when you're writing features _now_ that need to be released ASAP so the company can make money, the speed at which you write code is one of the most important factors to your productivity. Language ergonomics matter tremendously for this, affecting the time it takes to write, the chance for mistakes, and the amount of frustration you feel.
- ironmagma 6y agoError checking should also really be enforced by the language. It's too easy to forget otherwise.
- darthrupert 6y agoThank you for saying this. It seems like not many PL enthusiasts get this.