News for hackers. Stuff that matters.
Re:Using unwrap() in Rust is okay by jcranmer on Monday August 08, 2022 @12:34PM (#32387706)
The criteria I tend to prefer is "expected" versus "unexpected" errors. I/O errors, especially network errors, are things that are going to be expected under reasonable operation, therefore it make sense that code should handle them. Similarly, user input resulting in incorrectly formatted code should be reasonably expected and therefore handled.

But the same kinds of failures might not be reasonably expected in other circumstances--I wouldn't expect that the internal configuration files of an application should occur in reasonable operation, and therefore it makes sense to panic if they're corrupted... even if the cause is an I/O operation on a local disk, or parsing some JSON or TOML or INI or whatnot file.

One implication of this is that it needs to be easy for any error system to promote an "expected error" into an "unexpected error"--which is what unwrap/expect does. The recoverable/unrecoverable error suggests that there ought to be no reason to do this, but there is absolutely a reason to do so: what category an error falls into is ultimately decided by the context of the error, not the generation of the error itself.

Re:Using unwrap() in Rust is okay by vbezhenar on Monday August 08, 2022 @02:07PM (#32388921)
I write Java and for me it's not exactly "expected" versus "unexpected".

There are unexpected errors for sure. For example "StackOverflowError" which could be thrown from any method call.

There should be unexpected errors handler which does some sane thing in given circumstances. Usually it involves logging error and its details (stacktrace, may be something else) and returning some kind of generic error to the caller (e.g. HTTP 500).

But the thing is, this handler very often suitable for handling errors that I expect but I don't want to bother handling them. For example those errors might be rare enough that writing code would be a net negative (every line of code is maintenance burden and error handling code is maintenance burden power 2). And I'm totally OK with those errors being handled in generic way.

Even if it's user input sometimes. For example user could be me. And I know input format. I don't want to write error handling for myself, all I need is to prevent data corruption. Getting HTTP 500 InvalidNumberFormatException is totally fine in some situations.

And language should provide means for writing that kind of code. At least that's my opinion and that's what I truly miss in those languages with explicit error handling of every function call.

You might call it lazy coding. I call it reasonable coding.

Exceptions for the win!

Re:Using unwrap() in Rust is okay by spion on Monday August 08, 2022 @03:37PM (#32390025)
Rust does allow you to easily call functions or methods that may error by using the `?` operator (formerly `try!` macro)

https://doc.rust-lang.org/std/macro.try.html

You can also implement `From` (see `impl From` in the above doc) to define how you want external error types to be converted.

Its one of the rare languages where there isn't much boilerplate involved in using `Result`-style errors, even compared to exceptions (Swift is like this too)

There is even backtrace support for Result types: https://docs.rs/trace-error/latest/trace_error/

Re:Using unwrap() in Rust is okay by saghm on Monday August 08, 2022 @01:32PM (#32388485)
> The criteria I tend to prefer is "expected" versus "unexpected" errors

The way I've often heard this phrased is "exceptions are for exceptional behavior", and it's always rubbed me the wrong way a bit (although maybe this is partially just because I don't think wordplay is a sufficient argument to do something; I've made similar arguments in the past to friends who sung the praises of "no-shave November" and "thirsty Thursdays"). From digging a bit deeper when I've heard this opinion espoused, it seems like it mostly boils down to the fact that exceptions tend not to be as efficient as happy-path code, so using them for circumstances that are too common is not going to lead to good performance. I guess I don't really find this a subtle enough concept to warrant needing to introduce another abstraction layer into the discussion, especially one that's much vaguer like "is this behavior expected?" If there's a performance concern, I think it's much better addressed directly rather than shifting the discussion to a proxy.

Re:Using unwrap() in Rust is okay by spion on Monday August 08, 2022 @03:41PM (#32390078)
In languages with exceptions there typically isn't much of a distinction between expected and unexpected errors.
Re:Using unwrap() in Rust is okay by Someone on Tuesday August 09, 2022 @02:53AM (#32394956)
> I/O errors, especially network errors, are things that are going to be expected under reasonable operation, therefore it make sense that code should handle them

It makes some sense, but often, code should do relatively little and process should do most of the handling. Recovering from I/O errors and properly testing the recovery code can take huge amounts of time and effort.

Often, aborting the program and rerunning it or even restarting a service is the best way to handle these because properly handling them in code costs time better spent on other things. Logging a decent error message and aborting may be the better choice.

But of course, that depends on the use case. Databases need lots of recovery code for I/O errors and deadlock recovery, for example, even for cases that occur maybe once every year.

(And yes, process nowadays is often automated, making it code again, but IMO, that’s a different kind of code)

Re:Using unwrap() in Rust is okay by taeric on Monday August 08, 2022 @04:59PM (#32390941)
I'm a fan of, "is this going to a user, or to another part of the calling program?" If expecting it to go to a user, exception make sense. Need to bubble it all the way up to the part of the system that is closest to the user.
Re:Using unwrap() in Rust is okay by preseinger on Monday August 08, 2022 @09:17PM (#32393118)
The only thing that gets to make that decision is the call frame that initiated the program execution. Meaning -- only fn main gets to return stuff to the user. Everything else, all other functions, should (almost always) express faults as returned errors.
Re:Using unwrap() in Rust is okay by taeric on Monday August 08, 2022 @10:26PM (#32393613)
If you are writing a library, sure. If you are designing a system, this is silly. Functions aren't written in isolation. Such that there can be a broad definition of all exceptions that are expected to go to a user.

In the full system, you will have many side channels of data flow. Metrics and logging are also worth considering.

If you really want to boil a system down to a single entry point, at least do it like lisp, where the system has pluggable restart conditions defined.

Re:Using unwrap() in Rust is okay by preseinger on Tuesday August 09, 2022 @11:26AM (#32399285)
Functions should absolutely be written in isolation. That's the point of functions!

If I see this line of code

    result = val.method(foo, bar)
I make certain assumptions. Primarily that the implementation of val.method will in general only interact with foo, bar, and the fields of val. Metrics and logging should be dependencies injected to the val struct, not globals; expedience sometimes requires us to take shortcuts, sure, but those are exceptions, not patterns.

More fundamentally, that line of code, as it exists on the page, expresses an unambiguous flow of execution. Callers provide specific inputs to a function, the function goes off to do _something_ in a new level of the call stack, and, importantly, returns a result to me. Then I continue to the next thing.

The call stack underneath that function can be arbitrarily huge, sure. But I should be able to trust that when I read a sequence of expressions in my source code, the flow of execution through those expressions is exactly what it appears to be. Exceptions subvert this intuition. They make it so I can't know, or even predict, what will happen to my flow of execution when I jump into a new call stack. Every expression is potentially a return statement. That means I have to read the implementation of every function I call, recursively, in order to understand the execution flow of my code. This isn't tractable.

We obviously have to make some affordances for major failures, OOMs, div by 0, etc., so this isn't an absolute rule. But these have to be exceptional cases, not something that programmers need to consider as a matter of course. We simply don't have the cognitive capacity to model behavior in terms of recursively and arbitrarily complex implementation details. We need to be able to ignore this level of detail via simple and consistent abstractions.

Re:Using unwrap() in Rust is okay by taeric on Tuesday August 09, 2022 @12:50PM (#32400413)
Simple functions, certainly. But, not all things can be simple.

Consider your accelerator pedal in a car. Somewhat simple method to increase fuel to the engine. If something is wrong with the engine, it doesn't message that back to you through the pedal. Even though the pedal won't work. This can be as trivial as the car not being on, the pedal will not work.

So, if you are picking the straw man where every function has side effects and communicates back through a side channel, I agree. But if you are building a system where some things would require way more effort and code to get what you are aiming for, then we are in the realm of this article, where a panic that sends it back to the user makes far more sense.

Re:Using unwrap() in Rust is okay by preseinger on Tuesday August 09, 2022 @03:14PM (#32402651)
> Consider your accelerator pedal in a car. Somewhat simple method to increase fuel to the engine. If something is wrong with the engine, it doesn't message that back to you through the pedal. Even though the pedal won't work. This can be as trivial as the car not being on, the pedal will not work.

I don't see how this is a problem? The accelerator pedal API, so to speak, is purely mechanical -- its "return value" is whether or not you were able to put it to the position you wanted, it doesn't promise to return any information about the downstream effects of that pressure/setting.

The accelerator is tightly coupled to the valve which controls how much fuel is given to the engine, but anything after that is a downstream (event-driven?) effect that I as a driver have to observe through other means. So if the valve is stuck I expect to learn about that failure after I call this fn, sure. But the impact on the intake, or the engine RPM, or the transmission, or my wheel speed, or my actual velocity, these are all things I discover through other means.

So it's not

    fn depress_accelerator(f64 amount) -> CarState result
but rather something like

    fn depress_accelerator(f64 amount) -> bool worked
    fn depress_accelerator(f64 amount) -> f64 throttle
It's not complicated. When I call a function, I expect it will do what its signature says it can do, and I expect it to return control flow to me. If you can't rely on these assumptions, it's effectively impossible to build reliable software.
Re:Using unwrap() in Rust is okay by taeric on Tuesday August 09, 2022 @04:25PM (#32403781)
Well, first, the pedal is decidedly not coupled to the valve in many (most?) vehicles. It is hooked up to something else which is used as input to whatever is going to control the acceleration in the vehicle.

That said, this is precisely my point. Downstream from where my direct interaction is with the system, something can go wrong. And it doesn't make sense to think of it only in terms of a single "main." Instead, there are various parts of the system that all have different responsibilities. In particular, side channels of information are setup to route some errors/information back to a user that are not necessarily part of any individual function. And some systems should panic/fail instead of trying to continue, given the state of the system.

So, if something is being punted up from inside a software system, it makes sense to have an exception be there, to me. As it does, as well, to the article in this post. I can think this, all the while also agreeing that most of the time using return based responses makes a lot of sense.

Edit: Incidentally, I think your first function is more correct there. The state should also reflect where the accelerator pedal is currently sensed at. Any actual acceleration will be in response to the rest of the systems reading that state. There is literally no way for the pedal to know if it succeeded or not.

Re:Using unwrap() in Rust is okay by preseinger on Tuesday August 09, 2022 @09:05PM (#32406747)
> There is literally no way for the pedal to know if it succeeded or not.

Success for the pedal is defined in terms of what it can know, same as any other component in a system. The pedal is typically a mechanical device, so what it can know is restricted to its mechanical outcomes. Was the floor mat stuck behind it? If so, pressing the thing to 100% only results in an outcome of 50% depression -- maybe that's failure. And so on.

It's incoherent for depress_accelerator to return information about the car in which the engine it is connected to is installed. Does `ls /mnt/volume/file` return information about the chassis of the server in which the hard drive hosting `/mnt/volume` is installed?

> it doesn't make sense to think of [programs/systems] only in terms of a single "main."

Why not? You can absolutely model any program, composed of arbitrarily many parts/components, as a graph of components with a single root. I acknowledge that's not the only model, but it's definitely universal and effective.

> In particular, side channels of information are setup to route some errors/information back to a user that are not necessarily part of any individual function. And some systems should panic/fail instead of trying to continue, given the state of the system.

What do you mean by "errors/information ... not part of any individual function"? Isn't it the case that anything which could produce an error is necessarily part of a function i.e. a sub-tree of the execution call stack? If I write a fn doSomething(input x), why should the result of my implementation do anything, in general, other than return execution to my caller with an appropriate result value?

> So, if something is being punted up from inside a software system, it makes sense to have an exception be there, to me. As it does, as well, to the article in this post. I can think this, all the while also agreeing that most of the time using return based responses makes a lot of sense.

Functions define layers of abstraction. A given function knows only its own implementation, narrow properties of its caller (input parameters and output expectations), and the characteristics of the functions it calls as defined by the signatures of those called functions.

There should never be the concept of something "being punted up from inside a software system". A function is defined in terms of its inputs and outputs. Its success or failure is defined in terms of those things only.

Re:Using unwrap() in Rust is okay by merb on Monday August 08, 2022 @12:53PM (#32387965)
Network errors might be retryable/routable differently, but often (especially when starting out) should probably returned to the User. I mean if s3 is down you can retry the call but often it is down then
Re:Using unwrap() in Rust is okay by dymk on Monday August 08, 2022 @01:04PM (#32388128)
Sort of. At my company, if we removed retries from our services, our reliability would drop precipitously. Something like 99.99% of retries succeed on the second try, if there's not a hard service outage. If there is a hard outage, well, not much to do about that.
Re:Using unwrap() in Rust is okay by preseinger on Monday August 08, 2022 @09:20PM (#32393137)
Only the root of the call stack, fn main, should be able to return anything to the user. Everything else should return errors to their callers through the normal return mechanisms. Anything else, anything that introduces the possibility of shadow control flow, makes it basically impossible to maintain a working mental model of nontrivial programs.
Re:Using unwrap() in Rust is okay by preseinger on Monday August 08, 2022 @09:15PM (#32393102)
> I wouldn't expect that the internal configuration files of an application should occur in reasonable operation, and therefore it makes sense to panic if they're corrupted.

Whether an error returned from a fn is expected or unexpected has to be a property of the fn signature and language conventions in isolation. If semantic or other locally-unknowable details influence fault classification and error control flow at call sites, your program program becomes unmaintainable over time.

So like if you have fn parse_config that takes a string, or a file descriptor, or whatever -- it should return a Result<Config, Error> and yield an error for any un-parseable input. There is no reason this fn should ever panic.

IMO -- call stacks are sacrosanct, making control flow visible and obvious is one of if not the most important thing to optimize in nontrivial programming contexts.

Re:Using unwrap() in Rust is okay by throwawaymaths on Monday August 08, 2022 @05:25PM (#32391226)
It's kind of the opposite. If you have a network error, often you shouldn't bother to try handling it, the best strategy is often to zero out your state, give up and try again later.

By contrast, you should give up parsing a JSON if it's a config file read on startup but probably not if it's user input.