Hacker News new | ask | show | jobs
by Pxtl 4408 days ago
I tend to practice "only catch what you can handle" in exception-enabled languages - I haven't written in a systems language in almost a decade, mostly bad memories of C. How much does error-handling get in the way when you have to live without stack unwinding?
2 comments

So, normally in Rust, it's no problem to ignore the return value of a function. However, some types are tagged with the `#[must_use]` attribute, which makes it a warning at compile time to ignore the return value of any function that returns that type. Take the following program, which writes a buffer of bytes directly to stdout:

  fn main() {
      let mut out = std::io::stdout();
      out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]);
  }
The `.write()` method returns a Result type. The output of compiling this program:

  $ rustc pxtl.rs
  pxtl.rs:3:5: 3:53 warning: unused result which must be used, #[warn(unused_must_use)] on by default
  pxtl.rs:3     out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]);
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Again, it's just a warning, so the program will compile and run as expected:

  $ ./pxtl
  Hello!
If you really don't care about the return value here, the simplest (and probably best) way of appeasing this warning is to explicitly ignore the return type by making use of pattern matching:

  fn main() {
      let mut out = std::io::stdout();
      let _ = out.write([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]);
  }
In Rust, the underscore is a pattern that means "I don't care about this thing, completely ignore it". The advantage of using the underscore here rather than an actual variable, e.g. `let x = out.write(...)`, is that it will be impossible to refer to the return value later on and thus explicitly expresses your intent to ignore it. (Furthermore, if you assigned the return value to a variable and then didn't use it later on, Rust would emit yet another warning, this time for having an unused variable.)

The warning message alludes to a second way of silencing this error, which is by sticking the `#[allow(unused_must_use)]` attribute on top of your function. This will silence any warnings that arise from that function. If you wanted to disable this warning for your entire program, you could instead stick the `#![allow(unused_must_use)]` global attribute at the top of your program. Alternatively, you could compile the program with the `--allow unused_must_use` flag to completely silence all warnings of this type.

(One final note: in all cases where you see word "allow" used above, if you replace it with "deny" it will turn the warning into a compile-time error, thus enabling you to enforce a more rigorous error-handling strategy if you so choose.)

"only catch what you can handle" is incredible nonsense. Your code is the only code that knows how the code it's calling might fail -- it MUST catch all exceptions and either handle them, or re-raise them with a well defined type that is documented and declared in your API.

Anything else just leads to buggy software that has a try/catch block at the top level of the event loop/main/thread start function to deal with all the errors that leak out of its implementation and leave the process in an undefined state.

Exceptions are simply broken and awful. Java does them sorta right with checked exceptions, but the only safe thing is to not do them at all.

Obviously you should be converting exceptions that leave your library into other areas, but internally? Converting exceptions over and over and over again just means losing information from those exceptions, or worse hiding them. If I'm forced to dump a stack-trace to the text file, I want the exact exception that caused the problem, not some vague "Operation Exception" that quintuply wraps my actual desired exception, or worse completely threw it out to "cleaned it up for me" and tells me nothing about what went wrong.

I just helped a teammate work through a bug the other day where somebody decided to "handle" a case-sensitivity problem in their home-brewed SqlLite data-access code by simply returning null for the data member if you got the wrong case. This resulted in improperly-cased column names producing objects with null members - no error happened because they were valid SQL queries, but the dictionary-reading code was silently failing when it was reading the result-set. If the program had just blown up when there was a miss on the dictionary of column names? We would've quickly found out about that stupid case-sensitivity.

Defensive coding just means your bugs go non-local and become data problems instead of exceptions.

That doesn't make any sense. Defensive coding means not silently discarding errors (by returning null, in this case), and has nothing at all to do with exceptions.

As for rewrapping errors, yes, each subsystem should have its own error space. You don't lose data by nesting errors; on the contrary, each level can add additional context to an error result that makes debugging an unexpected issue far easier.

Tell that to the Erlang guys, who have been writing some of the most fault-tolerant code of the past two decades with an explicit catch-what-you-can-handle attitude by design.

Their failure model lies in proper task supervision, coding for the expected case, and letting errors propagate up to the task level, where you can either kill a task, log and handle, propagate, or do whatever you wish.

No, Erlang has been writing fault-tolerant code with an explicit functional, immutable design, with very explicit semantics for defining process supervision and restart at every point in the heirarchy.

That's not "catch-what-you-can-handle", that's "use functional programming and pervasive consideration of fault handling to ensure that you can handle faults at any layer".

Java's checked exceptions are the worst — a failed language experiment if there ever was one.

Thank Guava for Throwables.propagate().

The only failed language experiment are exceptions themselves.

I don't use Java APIs that don't throw checked exceptions; if your code does that, I won't even consider working at your place of business, because that means you don't understand that you've written a massive pile of ill-defined failure-prone code.

Unchecked exceptions are GOTO on steroids, and those GOTOs are part of the API contract. Java makes exception handling explicit and compiler checked -- hacking around checked exceptions makes exception handling implicit and human-checked, meaning that there's absolutely no static verification of a critical component of your API contract.

The problem isn't checked exceptions, the problem is that exceptions suck, and the only way to use them in a way that doesn't expose your code base to implicit GOTO failure modes is to use checked exceptions.

On our production software, we don't use exceptions at all, except where required by an API; instead, we always use monadic error handling. We have an uncaught exception handler for threads/thread pools/etc that does one thing: log the exception, and terminate the running Java process via System.exit(), allowing the process's watchdog to restart the failed process.

By its very nature, an uncaught exception is unexpected and places the process in an unknown state; the only safe thing to do is exit. Since the throwing of an uncaught exception triggers full process failure, it very much encourages defensive, safe practices that ensure that all error cases are handled and compiler-checked.

The result: our code is far more stable and reliable than any other project I've worked on, especially projects that have made use of runtime exceptions.

I won't even consider working at your place of business...

Praise be! The feeling is mutual. I agree to disagree.

Unfortunately, you'll lower the total value of the ecosystem by producing code and advocating practices that lower the level of reliability and correctness of code -- so agreeing to disagree doesn't really solve the issue that you write bad code.
So you're saying that it's not possible to write reliable and correct code in a language like C#, and not one C# programmer in the world is worthy of being a colleague to the great teacup50. That's the logical conclusion of your assertions, for all of its exceptions are unchecked, and it is otherwise semantically similar. If it's possible to write reliable and correct code in C#, then it's possible to write reliable and correct code in Java minus checked exceptions.

As far as APIs are concerned, the important thing is that the API is documented to throw something. It's not at all important that the compiler forces you to pollute either the immediate method's body or its signature and the body of the calling method, etc.