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?
The 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).
We define a custom error type for most functions in the Rust codebase I’m currently working on. It’s a little laborious but not that big a deal and I really enjoy being on the “completely precise” side of the spectrum :).
It still feels lighter weight than Java’s checked exceptions and I think maybe that comes down to all the language machinery that Rust offers around patten matching and error combinators.
I plan to blog about our error handling strategy... someday
Interesting. Although you're the first person I've heard about actually taking that approach, I have heard people lament that Rust library error types tend to be maximal rather than precise (IOW, peeved that a function's declared error type contains errors that that specific function can't actually produce; this is usually a jumping-off point to a spiel about asking Rust to support anonymous sum types), so I'm not completely surprised that somebody out there is actually doing it. That said, I've also heard people on the other side of the spectrum express that Swift's "I don't really care about the specific error type" is what they prefer; notably the anyhow crate (https://docs.rs/anyhow/1.0.32/anyhow/) is a good way to get that behavior in Rust while remaining idiomatic.
I think it is possible to automate some of our approach with a simple macro, I just haven't gotten to it.
I really like that you can look at a function and know every single type of exit it can have and know that you've handled them all.
But this might be because the thing that we are building (replicache.dev) runs in someone else's process so we need to be good citizens and be very careful to never panic or return unexpected/undocumented errors.
> the precise bookkeeping that so annoys people about Java's checked exceptions
This just came up in a different context (https://news.ycombinator.com/item?id=24448865) but I don't think it's the precise bookkeeping that annoys people about Java exceptions so much as the fact that you often can't be all that precise and yet you still need to do a lot of bookkeeping.
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.
Yes, that is still the case with 5.2. However, a proposal to add typed `throws` is currently being drafted and prototyped [0]. It won't be available in Swift 5.3, but if all goes well it might land in the next release after.
(Also, if you're doing Vapor programming, you may be pleased to know that Swift 6 is expected [1] to bring a real concurrency model, which will likely include some form of async/await so that you won't have to muck about with all those nested closures.)
Go 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.)
Go's approach is not great. You can leave off return assignment completely. You can't take a tuple as an argument. It has an amateur approach to how you can't shadow `err`, yet `:=` vs `=` change on whether `err` already exists or if your lhs variable is new. You have to do additional work to get a stack trace, errors are usually just strings. You may be forced to handle some errors (unless you trivially ignore them), but nobody is forced to make decent errors. Most libs don't even think about them, like being forced to handle `throw "something went wrong"`.
I like errors as return values (mainly Result<V, E>), but Go's is probably the worst impl I've come across. The reason why it's still productive despite this is because we, as developers, tend to not put much thought in errors anyways, so who cares if it sucks (until we need them).
The lack of a tuple type is the most frustrating for me, because it means you cant define channels as a type of tuple. You have to go through the boilerplate of defining an option type for every single T channel that you handle.
When I say 'option type' I actually mean the idiomatic go multiple return `(res, err)`. You can define a function to return that but its not actually a first class T, so you cant define a channel that follows the same semantics. That means if you have a bunch of functions that use the `(res, err)` style and you later want to convert them to, e.g., batch processing steps using channels, you have to now add a lot of boilerplate type definitions to tie things together.
Ah got it. Tuples come with their own challenges and are quite similar to structs, so I’m not surprised they didn’t make the cut. I’ve had to define structs like you say occasionally, but it literally takes like 2 seconds so I don’t really care, and appreciate not having to decide when to use tuples vs structs and argue with teammates about it too.
Youre right, it definitely isnt a large amount of work. Its the annoyance of the work and the leaky abstraction of "multiple returns" that _look_ (and sort of act) like its a tuple. In addition to channels, it also comes up if you want to store an array of return values.
even more annoyingly, you can define a function that returns it but not a function that accepts it, so you often have to jump through a lot of hoops to combine higher order functions with multiple return values.
Personally I find the Rust syntax is a nice middle ground. If the function returns a Result, it can throw an error. And unless you provide a From<> implementation you do need to handle the specific error that gets thrown. But if you want things to bubble up cleanly you provide From<> (or use a crate that provides it for you) and it all works wonderfully.
I'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.
You do and you don't. If you're using a Java library written by someone who doesn't believe in checked exceptions in Java, you might be lulled into thinking everything will be fine.
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.)
HA! 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.
>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.
> 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.
Checked 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.
Since 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.
Sorry 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).
They 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.
Hejlsberg is an awesome compiler maker. His Turbo Pascal compiler was incredible -- in approx 30KB he wrote a compiler, full screen editor and library. He's not that good as a language designer. C# is basically a clone of Java with broken exception handling.
The 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).
The 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.
You'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.
To add on to what other commenters have pointed out with `?`, there's also the interesting [fehler crate](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.
Pretty 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.
> 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.
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.
Change your recursive-descent parser so it pulls source tokens from a socket as opposed to RAM. All networking IO is expected to fail. You probably don’t want to handle socket errors in the parser because there’s nothing you can do about them there. You want to handle them very far away, telling user you’re unable to parse stuff due to a network error.
Some libraries expose parallel APIs for these two cases. For example, hash maps from C# standard library exposes both operator[] which throws exceptions when trying to get a value that does not exist, and TryLookup which returns a boolean.
Overall, I think exceptions are solid in C#. There’s only one class of exceptions there, they can include lower-level ones, can aggregate multiple of them, for native interop they have 32-bit integer codes and runtime support to converting codes to/from exceptions, have runtime support to preserve contexts to re-throw later (e.g. on another thread). Uncaught exceptions become hard crashes.
> Overall, I think exceptions are solid in C#. ... Uncaught exceptions become hard crashes.
Do you see the problem, these two sentences are juxtaposed? When exceptions are not part of the contract you would have to read the source code of every method you call (and methods they call) to know what exceptions to catch.
Even then, a method you call can be modified later to throw a new exception and your code will crash. The only solution is to use the root Exception class which everyone agrees is a bad idea.
> When exceptions are not part of the contract you would have to read the source code of every method you call (and methods they call) to know what exceptions to catch.
Even that won’t help you to find that out when the API takes callbacks or interfaces.
> is to use the root Exception class which everyone agrees is a bad idea
It allows to use lambdas (called delegates but that’s technicalities) and interfaces in APIs. Unlike C++ which allows to throw anything (probably because the standard library with std::exception was too late to the party), in C# the language guarantees that will cover 100% of the exceptions.
The idea is decades old and probably predates exceptions. For example, some pieces of Linux kernel APIs say in the documentation something like “return negative error code if failed”. What you see in almost 100% of use cases of error handling is if( result < 0 ) condition, not a switch-case testing for individual codes or their ranges.
Catching individual exception types is usually a bad idea. Software is incredibly complicated these days; we use tons of APIs and libraries implemented by unrelated people from different continents.
If the language doesn’t make them part of the verifiable contract, you’ll get runtime errors once the implementation changes and a new version starts throwing another exception type.
If the language makes them part of the contract, you’ll be wasting a lot of time making your code compile again after a dependency is updated. This creates an incentive to never update them.
>Even then, a method you call can be modified later to throw a new exception and your code will crash. The only solution is to use the root Exception class which everyone agrees is a bad idea.
This view was (is?) very widespread in the Python community, which led me astray as a beginner. Trying to find each individual exception that a function can throw when you really only care if it errored or not is a fool's errand. These days I just catch the base Exception, and only catch specific subclasses of it if there's an actual need to.
>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.
Go 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.
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:
Compare to the same code written in a language that supports checked exceptions: 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?