My takeaway from the last 30 years in computer science is that errors are not exceptional. They occur often and should be accounted for near the code that generates the errors.
Exceptions and return values are both sub-optimal. Exceptions encourage drastic actions for non-drastic events (exit the program if an HTTP server is transitently slow). Return values encourage ignoring the error value, and then wondering why your program broke. Special types that wrap error values or exceptions cause the same problem; when you want to defer something, your code becomes contaminated with the error type (f(x) returns an error, g(x) calls f(x) but doesn't feel like dealing with the erro, so now g(x) returns an error... and all the way it goes up to the top level.)
Overall, I don't see a grand unified solution to this problem. We should make it possible for functions to declare everything that goes wrong so that recovery can be more easily tested. No language does this; they often merge vastly different errors into the same type, so the programmer is powerless to understand the possibilities. Consider two database errors; "syntax error" and "transaction aborted, retry it". Typed error systems typically condense that to a "database error", but how your program should handle the two cases are vastly different.
Anyway, I'm happy with the way go works. If I don't explicitly know how to handle an error, I wrap it with a tag and return it. When I look at the alert / error logs, I know which codepath caused the problem and can investigate. For cases where I know how to handle an error, I can explicitly deal with it (yes, often with strings.HasSuffix to find the one I know how to handle). That is all I really need. If it were an exception, the code would be basically the same. So I think it's a red herring to complain about values vs. exceptions. Neither system prevents you or encourages you to write correct, robust code. If we want to do that, we need completely new tools.
Rust's type system + the failure crate (https://crates.io/crates/failure) is the nicest I've seen. It's similar to Haskell in that errors are part of the return value of a function, and the type system enforces handling of this.
_But_ Rust also includes some really nice syntax for passing errors through so I can write this:
The `failure::Error` type automagically wraps any error. That means I can go through many levels of my stack returning `failure::Error` and at any point in the call chain I can decide to examine the error type instead of writing `some_func()?;`, which would pass the error back.
The only part I don't like is that I need to add a `?` when I return my own errors. I don't totally understand this but from what I sort of understand the reason is that `?` invokes `.into()` on the returned object, which is what lets me return a (wrapping) `failure::Error` instead of a `MyPackageError`.
The upshot of all this is that "ignoring" errors is quite easy, as long as _somewhere_ in my call chain I actually do handle the error. The type system will enforce this for me, as attempting to treat a `Result<String, Error>` as just a String will cause a compile time error. I have to unwrap it and either ignore the error (which would lead to a runtime panic if there _was_ an error) or do the right thing, which is to explicitly handle both the ok and error cases.
I'm working on a CLI program using this system, and I can bubble all the errors up to the main entry point of the CLI. At that point I can turn errors into a print to stderr and an appropriate exit code.
The two are closely related concepts. You can model checked exceptions as a secondary return type, except without the same first-order representation (e.g. inability to specify checked exceptions in generic terms).
> My takeaway from the last 30 years in computer science is that errors are not exceptional. They occur often and should be accounted for near the code that generates the errors.
This is exactly opposite my experience. In 90% of cases there is no way to recover from an error locally and I should just fail at the highest level, possibly retrying a few times.
If that's correct, then our systems are built on false assumptions. SSDs should not see an ECC error and write the data to a new block; that error should kill your program. TCP should not detect packet loss and retransmit, it should kill your program?
My point is, a lot of good error handling is built at the lowest level, close to the point of failure. That code knows what the problems are and how to fix them. Errors are not the exception, they are the rule. If you handle it well, people won't even know how unreliable the underlying system is. (NAND flash? The first time I used raw NAND flash I was blown away at how unreliable it was. How do computers even work at all like this, I thought. Then I realized... that's why so much money is poured into things like SSD controllers. To hide that fact from the user and make them happy, even if the raw technology it's built from doesn't offer perfect reliability.)
>My point is, a lot of good error handling is built at the lowest level, close to the point of failure.
This is very true for some types of errors and for some types of programs but not true at all for others, which is why this debate has been going on and on for decades.
An extremely common example for the latter is programs that touch a DBMS or file system on every other line. You don't want to see error handling code for "database is gone" or "local disk is gone" events all over the place.
The information required to handle these errors just doesn't exist in the local context. So the only reasonable question is how to let those errors bubble up to some central location where you can handle them or decide to abort.
In my opinion there are good arguments for and against both exceptions and error returns. But if error returns are used, then there must be some reasonably ergonomic way to do it, i.e not what Go does right now.
I just don't agree with adding more syntax to the language for what basically amounts to an if statement. If it's a conditional, which it is, just use an if statement.
Look at all the monad tutorials for Haskell. Nobody knows how to use the special syntax for Maybe and Either. They read hundreds of articles and still don't get it. Meanwhile, everyone understands how "if err != nil" works, perhaps too well, which is why they complain about it.
> My takeaway from the last 30 years in computer science is that errors are not exceptional. They occur often and should be accounted for near the code that generates the errors... We should make it possible for functions to declare everything that goes wrong so that recovery can be more easily tested.
> Accidents happen. What he meant is that they must happen. Worse, according to Perrow, a humbling cautionary tale lurks in complicated systems: Our very attempts to stave off disaster by introducing safety systems ultimately increase the overall complexity of the systems, ensuring that some unpredictable outcome will rear its ugly head no matter what.
Exceptions and return values are both sub-optimal. Exceptions encourage drastic actions for non-drastic events (exit the program if an HTTP server is transitently slow). Return values encourage ignoring the error value, and then wondering why your program broke. Special types that wrap error values or exceptions cause the same problem; when you want to defer something, your code becomes contaminated with the error type (f(x) returns an error, g(x) calls f(x) but doesn't feel like dealing with the erro, so now g(x) returns an error... and all the way it goes up to the top level.)
Overall, I don't see a grand unified solution to this problem. We should make it possible for functions to declare everything that goes wrong so that recovery can be more easily tested. No language does this; they often merge vastly different errors into the same type, so the programmer is powerless to understand the possibilities. Consider two database errors; "syntax error" and "transaction aborted, retry it". Typed error systems typically condense that to a "database error", but how your program should handle the two cases are vastly different.
Anyway, I'm happy with the way go works. If I don't explicitly know how to handle an error, I wrap it with a tag and return it. When I look at the alert / error logs, I know which codepath caused the problem and can investigate. For cases where I know how to handle an error, I can explicitly deal with it (yes, often with strings.HasSuffix to find the one I know how to handle). That is all I really need. If it were an exception, the code would be basically the same. So I think it's a red herring to complain about values vs. exceptions. Neither system prevents you or encourages you to write correct, robust code. If we want to do that, we need completely new tools.