4 ms·
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 re
by jcranmer 4y ago
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.
- merb 4y agoNetwork 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
- dymk 4y agoSort 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.
- preseinger 4y agoOnly 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.
- saghm 4y ago> 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.
- spion 4y agoIn languages with exceptions there typically isn't much of a distinction between expected and unexpected errors.
- vbezhenar 4y agoI 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!
- spion 4y agoRust 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 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/ https://docs.rs/trace-error/latest/trace_error/
- taeric 4y agoI'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.
- preseinger 4y agoThe 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.
- taeric 4y agoIf 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.
- preseinger 4y agoFunctions 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.
- throwawaymaths 4y agoIt'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.
- preseinger 4y ago> 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.
- Someone 4y ago> 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)