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