| > Personally, i really like having multiple return values, since being able to give a function multiple inputs but only being able to return a single thing always felt weird - if your require any metadata in a language like Java, then you'd have to come up with wrapper objects and so on. MRV is nice and useful, and “error as value” languages usually have ways to return multiple values (usually in the form of tuple), but it’s not proper and correct for error signalling, because the error and non-error are almost always exclusive. In that case, using MRV means you have to synthesise values for the other case (which makes no sense and loses type safety), and that you can still access the “wrong” value of the pair. > To me, that's an example of "opt in" error handling, which in my eyes should never be the case. The compiler should force you to handle every exception in some way, or to check for it. That is what Rust does (including a clear warning if you drop a `Result` without interacting with it at all), although for convenience reasons (because it doesn’t have anonymous enums and / or polymorphic variants) the errors you get tend to be a superset of the effectively possible error set. Though that’s also a factor of the underlying APIs, when you call into libc it can return pretty much any errno, the documentation may not be exhaustive, and the error set can change from system to system. Plus the error set varies depending on the request’s details (a dependency which again may or may not be well documented and evolving). So when you call `open(2)`, you might assume a set of possible errors which is not “everything listed in errno(3) and then some”, but a wrapper probably can not outside of one that’s highly controlled and restricted (and even then it’s probably making assumptions it should not). |
I actually agree with Rust's choice here. You, the programmer, know whether some particular error is something you can cope with or not and it's appropriate to panic in the latter case. Where you draw the line is up to you, in a ten line demo chances are "the file doesn't exist" is a panic, in your operating system kernel maybe even "the RAM module with that data in it physically went away" is just a condition to cope with and carry on.
My litmus test here is Authenticated Encryption. The obvious and easy design of the decrypt() method for your encryption should make it impossible for a merely careless or incompetent programmer to process an unauthenticated decryption of the ciphertext. This makes most sense if you have an AE cipher mode, but it was already the correct design for both MAC-then-Encrypt or Encrypt-then-MAC years ago, and yet it's common to see APIs that didn't behave this way especially on languages with poor error handling.
In languages with a Sum type Result like Rust, obviously the plaintext is only inside the Ok Result, and so if the Result is an Err you don't have a plaintext to mistakenly process.
In languages with a Product type or Tuple returns like Go, it's still easy to do this correctly, but now it's also easy to mistakenly fill out the plaintext in the error case, and your user may never check the error. Dangerous implementations can thus happen by mistake.
In languages with C-style simple returns, it's hard to do this properly, you're likely using an out-buffer pointer as a parameter, and your user might not check the error return. You need to explicitly clear or poison the buffer on error and even then you're not guaranteed to avoid trouble.
In languages with Exceptions, the good news is that the processing of the bogus plaintext probably doesn't happen, but the bad news is that you're likely now in a poorly tested codepath that isn't otherwise taken, maybe far from the proximate cause of the trouble. Or worse, your user wraps your annoying Exception-triggering decrypt method and repeats one of the above mistakes since they don't have better options.