[HN Gopher] Where Everything Went Wrong: Error Handling and Erro...
       ___________________________________________________________________
        
       Where Everything Went Wrong: Error Handling and Error Messages in
       Rust (2020)
        
       Author : lukastyrychtr
       Score  : 145 points
       Date   : 2021-02-19 08:57 UTC (14 hours ago)
        
 (HTM) web link (msirringhaus.github.io)
 (TXT) w3m dump (msirringhaus.github.io)
        
       | protoman3000 wrote:
       | Codepaths that can produce errors are just a monad. Separate this
       | code from the rest and you have a solution for the problem.
       | 
       | It's Haskell's do all over again and pure vs impure.
        
       | jimis wrote:
       | I find this article hard to read. But I believe its point stands.
       | In Rust:                 - You bubble up errors using `?`
       | operator,       - then you get a nice error message.       -
       | However the location of the error is lost, the more complex the
       | program, the harder it is to figure out where "permission denied"
       | for example comes from.
        
         | nivenkos wrote:
         | You can attach context to the errors though.
         | 
         | It's only really a pain when writing libraries as you have to
         | be so specific about your error types, etc.
         | 
         | Overall, it's probably still the "least bad" option compared to
         | other languages' approaches (both Go and Python are painful for
         | this for example). But it can be a lot of extra work sometimes,
         | especially when just working on the first basics of a library
         | crate.
        
           | magicalhippo wrote:
           | > You can attach context to the errors though.
           | 
           | I'd say you _should_ attach context.
           | 
           | If there's problems accessing a file, then having an error
           | message without the filename in it is near useless.
        
         | jandrese wrote:
         | The whole article was basically the author figuring out how to
         | get a full path error message instead of only getting the first
         | or last component of the error path. Also, how to do it without
         | also crashing the program entirely. Plus of course getting some
         | context about the state of the program when it crashed, and
         | without imposing the full stack trace overhead since this error
         | was supposed to be available even on production builds.
        
       | the8472 wrote:
       | > 'Dumping failed: "Failed in ptrace::read: Sys(EIO)"'.
       | 
       | Or instead of trying to wrangle error messages, when a syscall
       | falis you can strace or perf trace to find the offending stack,
       | even in 3rd-party code, as long as it has debug info.
        
       | Veserv wrote:
       | The author appears to be trying to use error messages in their
       | own code to debug it which is a pretty weird way to use error
       | messages. Error messages are for humans and users. If you are
       | developing your own code you can just use a debugger to debug the
       | problem.
       | 
       | Assuming they only had access to basic tools they could have just
       | plopped down a breakpoint in the relevant error return from
       | copy_from_process() and slowly walk up to discover their nullptr
       | error pretty quickly without any code changes. If they had modern
       | tools they could have just turned on tracing and then run
       | backwards to figure that out and where the nullptr came from.
        
         | iudqnolq wrote:
         | I've found the rust debugging experience to be very primative.
         | When you say modern tools, are you describing rr? As far as I
         | know that doesn't reliably integrate with rust?
        
         | jiehong wrote:
         | It could also just be a problem that appears in production,
         | where there is no way to add breakpoints after the fact (even
         | more so if this only occurs very rarely).
        
       | jgilias wrote:
       | The rule of thumb for me is `thiserror` for libraries, `anyhow`
       | for executables. Seems to work well enough in the vast majority
       | of cases. I do agree that the Rust way can be frustrating at
       | first. But then, at some point it becomes clear that being forced
       | to keep your error conditions in mind at all times is actually a
       | healthy thing. Then going back to languages where code may fail
       | anywhere seems less than optimal. Sort of like the difference
       | between securing your ropes and just crossing fingers.
       | 
       | As a side note, I've found that getting used to `Result` actually
       | gives an added benefit of getting comfortable with the idea of
       | such computational contexts in general. Be it Options, Maybes,
       | Futures, Promises, Haskell IO, or anything of that kind.
        
         | Floegipoky wrote:
         | I'm imagining a sort of "wax on, wax off" moment but with
         | monads.
        
         | zvrba wrote:
         | > The rule of thumb for me is `thiserror` for libraries,
         | `anyhow` for executables.
         | 
         | And as the executable gets larger and refactored, some of the
         | "executable" code will inevitably become "library" code. What
         | then?
        
           | masklinn wrote:
           | > And as the executable gets larger and refactored, some of
           | the "executable" code will inevitably become "library" code.
           | What then?
           | 
           | Then that is migrated to `thiserror` (or something bespoke)
           | at the same time as it's migrated to the library context.
           | 
           | The library / executable is the delineation between providing
           | a Rust-level API for third parties versus consuming such
           | APIs.
           | 
           | If you're providing a Rust API, you want to provide precise
           | error so that the user is able to precisely target and handle
           | errors _if they need to_.
           | 
           | If you're only consuming Rust APIs, then you want to
           | precisely handle some of the errors you get, and just chuck
           | the rest over the side.
        
           | lpghatguy wrote:
           | There's nothing stopping you from continuing to use anyhow in
           | a library, it just doesn't produce error types with much
           | structure.
           | 
           | You can continue returning `anyhow::Error`, wrap it into a
           | newtype error for your library, or refactor to use thiserror.
           | Thanks to the `?` operator and Rust's type aliases, you can
           | get away with very few code changes to switch between these!
        
         | da-x wrote:
         | Why not `thiserror` for executables as well? It happened to me
         | a few times that I started to write an executable program, but
         | then realized I want to embed its functionality in a library.
         | Converting from `anyhow` to `thiserror` at that stage would be
         | extra work that can be avoided.
        
           | masklinn wrote:
           | > Why not `thiserror` for executables as well?
           | 
           | You can absolutely do that if you want, it's just that
           | _usually_ when writing an executable you don 't care about
           | creating the precise error types thiserror provides
           | (especially doing so executable-wide), you'd handle the
           | errors you get from reqwest or sqlx or whatever when you get
           | them, and those you don't handle you just want to bubble up
           | to an executable-wide handler.
        
           | jgilias wrote:
           | I guess this depends somewhat on the situation. If the design
           | is pretty clear upfront with a part that can be implemented
           | as a core library, I'd make the library use `thiserror` from
           | the beginning. However, if it's not really clear and I have
           | to start with exploratory coding, then keeping track of error
           | types that may come and go feels like unnecessary overhead,
           | when I can just use `anyhow`.
           | 
           | But! To each their own!
        
             | status_quo69 wrote:
             | I've used `thiserror` with great success in small side web
             | projects since you can create a new enum of Errors that can
             | be converted from a class of underling library errors.
             | Then, inside of my regular application code, I sprinkle in
             | `anyhow` to make my life easier.
             | 
             | For example, if I wanted to say, return a 500 status code
             | for all diesel database errors, I can convert the diesel
             | error into my custom error type, then throw it back up the
             | stack using `anyhow`. This works _really_ well in
             | conjunction with Rocket's Responder impl.
             | 
             | EDIT: This is pretty close to what TFA is saying as well, I
             | should have read more in the article, heh
        
           | marcosdumay wrote:
           | Usually, you don't have any good thing to do with the error,
           | so keeping it around just makes your code worse.
        
           | TheCoelacanth wrote:
           | thiserror is quite a bit more work to use. There's nothing
           | wrong with using it for applications, but it's often overkill
           | for that use case
        
       | nullc wrote:
       | Someday I will attempt to use some rust software from the
       | internet without almost immediately running into a panic with
       | some inscrutable message.
       | 
       | Maybe that day will be a day after _f$#$@@#$_ _crashing_ stops
       | being the easiest and most idiomatic way of handling unexpected
       | conditions.
        
         | pferde wrote:
         | I prefer it when a program just gives up and crashes when it
         | encounters an unexpected condition, instead of trying to
         | soldier on and later possibly corrupting some data or state I
         | care about, because it was working under incorrect assumptions.
         | 
         | Now, the amount of unexpected conditions should be kept to a
         | minimum, essentially just things outside of control of that
         | program that the program cannot verify reliably. The rest
         | should be _expected_ conditions, and error-handled properly.
        
         | unrealhoang wrote:
         | Curious: which Rust software that you immediately ran into
         | panic with?
        
         | marcosdumay wrote:
         | You can't just blindly copy Rust code from the internet. It
         | doesn't work.
         | 
         | You can't blindly copy code in C#, Java, Javascript, Go,
         | Python, C, or any other language either, but some of those will
         | try very hard to hide the fact that your code isn't working.
        
       | Animats wrote:
       | Python eventually reached a good system, with an official
       | exception hierarchy. Originally, exceptions could be any type. By
       | Python 2.7, new exceptions had to be derived from something
       | already in the official tree. If you catch an exception in the
       | tree, you also catch anything subclassed from it.
       | 
       | You want a hierarchy where there's a subtree for external events,
       | like network and file issues, and a subtree for internal program
       | failures. That lets you catch external events and retry or
       | something.
       | 
       | Python 3.x has a different exception hierarchy, and it's worse.
       | Too much is too close to the root, which leads people to catch
       | "Exception". That catches internal program errors along with
       | network errors, which is not helpful.
        
       | jstrong wrote:
       | as someone who works 95% of the time in rust, how to handle my
       | errors is never a problem for me. I think the issue is the
       | expectations of people coming into it with a different frame of
       | reference, expecting the kind of traceback/exception framework
       | built in - and for that to be a typical debugging workflow.
       | 
       | like 95 times out of 100, I just `.map_err(|e| /* code to convert
       | e to return type's error... */)` and I'm done. the other 5 times
       | I create a local error enum to represent the different types of
       | error that can be returned.
       | 
       | most importantly, the overwhelming gladness I have that code
       | somewhere deep in my stack can't throw an exception vastly
       | outweighs the "pain" of the ongoing incremental work it takes to
       | handle errors intelligently throughout my code.
        
         | pedrocr wrote:
         | Rust also has the traceback/exception error handling mode in
         | the panic cases. And since there's no standard way to check
         | those at compile time you're left to fuzz the code to find all
         | the possibilities (or use some hacks). It would be great if the
         | compiler could be put into a mode where panics are handled like
         | errors that need to be explicitly handled. Maybe something
         | like:
         | 
         | 1) At the function level or crate level being able to specify
         | if the code can or can't panic, sort of like safe/unsafe
         | function signatures.
         | 
         | 2) Being able to have blocks of code return MaybePaniced<T> and
         | have to turn that into an error or other result. If it's not
         | handled and the function/crate is marked as nopanic the
         | compiler rejects the code.
         | 
         | It would make the code more robust, and allow for an ecosystem
         | where just like nostd you can specify that your crate is
         | nopanic which helps in some environments.
        
           | johnsoft wrote:
           | I found a crate that claims to do #1:
           | https://github.com/dtolnay/no-panic
           | 
           | This also looks interesting:
           | https://github.com/Technolution/rustig
        
             | pedrocr wrote:
             | Yep, the no-panic crate is the hack I mentioned. It's using
             | the linking process to fail compilation if I remember
             | correctly. It only works on individual functions. rustig I
             | didn't know and looks very interesting, thanks. Having it
             | integrated in the compiler as annotations and guarantees
             | would be ideal.
        
         | rklaehn wrote:
         | Rust error handling is great for reliable systems. But it used
         | to be really annoying when just doing exploratory coding.
         | 
         | With the anyhow crate, this problem is solved as well. Just use
         | anyhow if you just want to try something out quickly without
         | being slowed down by error type mismatches, and then later
         | refine it to a handcrafted error type.
         | 
         | I prefer rust error handling over all other languages I worked
         | with (scala, java, C++, C, javascript, typescript, ...).
        
           | egnehots wrote:
           | for exploratory coding you can also just unwrap, assert,
           | panic.. It's useful to remember that error handling is
           | optional in those cases :)
        
             | unrealhoang wrote:
             | anyhow with ? is even shorter than unwrap. If it's likely
             | to have error at some point, I'd throw in a .context, it is
             | so convenient. Edit: typo.
        
               | edflsafoiewq wrote:
               | ? requires the return type be annotated with an error
               | type and the success case be annotated with Ok, for all
               | functions up the stack, between the current function and
               | where handling occurs. unwrap is purely local.
        
           | nwellnhof wrote:
           | > Rust error handling is great for reliable systems.
           | 
           | Last time I checked, Rust couldn't even catch malloc
           | failures.
        
             | wizzwizz4 wrote:
             | To be fair, these days malloc _doesn 't fail_; your program
             | crashes when it tries to use the memory, and there's
             | nothing you can do about it. (But I agree that this is a
             | problem with Rust's alloc system.)
        
           | simias wrote:
           | Completely agree. I used to create my own error type(s)
           | manually and implement the various conversions for 3rd party
           | crates and it was quite a lot of boilerplate but with anyhow
           | (for applications where you don't really care about strict
           | error types) and thiserror (when you need to be a little more
           | thorough).
           | 
           | Actually that's effectively TFA's solution, if they had used
           | thiserror/anyhow from the start there might not have been an
           | issue to begin with. Admittedly it took me a while to find
           | these crates since they're not part of std.
           | 
           | Maybe the community should come together to create a curated
           | list of third party crates that should probably be known by
           | all Rust devs? Crates like thiserror, anyhow, rand, serde,
           | clap and other "de-facto standard" crates?
        
             | wronghorse wrote:
             | This is really interesting because I recently found the
             | anyhow crate and I was wondering why it's not mentioned
             | anywhere else. Definitely feels like useful information
             | that a newly minted Rust dev could make use of.
        
               | simias wrote:
               | Indeed. I wish I learned about it earlier myself, it
               | would've saved me some boilerplate in the past.
        
       | the_mitsuhiko wrote:
       | At this point I'm quite happy with error handling in Rust. I just
       | wish the backtrace member on the error trait was not nightly only
       | and we had the ability to attach backtraces to errors.
        
         | conradludgate wrote:
         | I'm pretty sure eyre[0] can provide backtraces in stable using
         | `stable-eyre`
         | 
         | [0]: https://docs.rs/eyre/0.6.5/eyre/
        
         | tiddles wrote:
         | I've migrated a few big projects through several iterations of
         | different error handling crates, which was always painful. For
         | now I've settled on thiserror, but only until backtraces are
         | stable - then it's back to the churn :)
        
       | kstenerud wrote:
       | Error handling has been wrong since the beginning, and has
       | continued to be wrong ever since.
       | 
       | First, we had error codes. Except these were wrong because people
       | forget all the time to check them.
       | 
       | Then we had exceptions, which solved the problem of people
       | forgetting to check by crashing the app.
       | 
       | Then the Java team got the bright idea to have checked
       | exceptions, which at first helped to mitigate crashes from
       | uncaught exception, but caused an explosion in thrown exception
       | signatures, culminating in "catch Throwable". Back to square one.
       | 
       | Then we got multi-return error objects, maybes, panics, and all
       | sorts of bright ideas that fail to understand the basic premises
       | of errors:
       | 
       | Any error system that relies upon developer discipline will fail
       | because errors will be missed.
       | 
       | Any error system that handles all errors the same way will fail
       | because there are some errors we can ignore, and some errors we
       | must not ignore. And what's ignorable/retriable to one project is
       | not ignorable/retriable to another.
       | 
       | Attempting to get the complete set of error types that any given
       | call may raise is a fool's errand because of the halting problem
       | it eventually invokes. Forcing people to provide such a list
       | results in the Java problem for the same reason.
       | 
       | It's a hard problem, which is why no one has solved it yet.
        
         | infensus wrote:
         | > Then we got multi-return error objects, maybes, panics [...]
         | that fail to understand the basic premises of errors:
         | 
         | > Any error system that relies upon developer discipline will
         | fail because errors will be missed.
         | 
         | > Any error system that handles all errors the same way will
         | fail because there are some errors we can ignore, and some
         | errors we must not ignore.
         | 
         | Not sure how that's true for Rust. Functions that can fail,
         | return a Result, that contains either a value or an error. If
         | you want to use the value, you have to either explicitly check
         | which one is it (you can handle or ignore the error at this
         | point), or quickly access it by unwrapping the Result, which
         | will crash (panic) the program on error.
        
         | enriquto wrote:
         | > Error handling has been wrong since the beginning
         | 
         | 100% this. The very concept of "error" is philosophically
         | unsound. There are no errors; only conditions that you dislike.
         | It is unfortunate that programming languages allow to express
         | your emotional detachment to one of both cases of a branch.
         | Nothing good can come from that.
         | 
         | I yearn for a language with no error handling nor exceptions.
         | Just plain language constructs to deal with the state of the
         | world around the program, without unnecessary emotional
         | attachment to certain flow paths.
        
           | blacktriangle wrote:
           | Maybe CL got this right with the condition/restart system? In
           | addition to their utility for experimental programming,
           | conditions work really well for handling errors
           | programatically.
           | 
           | The general problem seems to be something weird happening far
           | down in the system, for which the correct way forward is
           | dependent on how we got there. This situation can't be
           | handled where the issue happened since that would break
           | encapsulation, but unrolling the stack all the way up blows
           | away the context that is necessary to understand how to move
           | forward. I don't see how a system that either unwinds the
           | stacks (throwing errors) or tries to encapsulate the state
           | through the system (Either) would truly solve the issue.
        
             | enriquto wrote:
             | computers don't do "weird" things. If you think this is the
             | case it means that the interface you are using is
             | incomplete or not well-specified.
        
             | finder83 wrote:
             | I agree, for the actual handling part, I think CL's error
             | system is the best I've seen, and I'm surprised that more
             | languages don't implement a similar system.
             | 
             | It doesn't solve the issue of forcing programmers to handle
             | errors..but then, CL doesn't care a lot about hand holding.
        
               | masklinn wrote:
               | One advantage of condition systems is that the caller
               | gets to decide whether the condition is even an error.
               | Though the restarts are still under the control of the
               | callee.
        
           | carapace wrote:
           | Aye. throw/raise is GOTO.
           | 
           | Forth?
        
             | enriquto wrote:
             | > throw/raise is GOTO.
             | 
             | Worse, it's COME FROM!
             | 
             | https://en.wikipedia.org/wiki/COMEFROM
        
               | simiones wrote:
               | To be fair, throw is GOTO. Catch() is COMEFROM :).
        
               | carapace wrote:
               | Ah, my peeps! I love you guys.
               | 
               | enriquto when you talk about "a language with no error
               | handling nor exceptions" it reminds me of a crazy idea i
               | was toying with: what if you made all "exceptions"
               | require handlers, e.g. what if every divide had to be
               | accompanied by code to deal with divide-by-zero? In other
               | words, DIV(X, Y, Foo) would be (X/Y if Y != 0 else Foo())
               | And so on...
        
               | heja2009 wrote:
               | that's the way some embedded control systems are
               | designed. with some (many) errors (can't read/write)
               | being fatal and shutting down the system after an alarm
               | is sent.
        
               | Ace17 wrote:
               | Technically, `throw` is `goto somewhere`.
        
               | marcosdumay wrote:
               | I guess gettout wasn't well accepted by the committees of
               | the time.
        
           | randyrand wrote:
           | A better word might have been "failure".
           | 
           | Something is a failure when it fails to do what it says it
           | does.
           | 
           | openfile();
           | 
           | If openfile() does not open a file, then it failed.
           | 
           | This terminology makes it clear we are not just bubbling up
           | errors. It's the function itself which failed.
        
           | kazinator wrote:
           | Well, no, there are errors, and then there are environmental
           | situations (if we ignore, for a minute, hardware
           | malfunctions).
           | 
           | like "file not found", "disk full". Something in between like
           | "out of memory".
           | 
           | Misusing an object as the wrong type, or division by zero, or
           | accessing missing memory are errors. These usually point to
           | defects in the program, or in some cases defects in the
           | program's defense against bad inputs.
           | 
           | There are related errors are the hardware level, like numeric
           | exceptions and bus errors. Without these, machine-language
           | programs just lock up or produce garbage results.
           | 
           | Situations like "file not found" or "host not reachable" are
           | usually environmental conditions, and not internal problems.
           | There are ways in which they can be internal problems; two
           | modules in a program might be related in such a way that one
           | prepares a file that the other expects to exist, and there
           | can be some bug in that.
           | 
           | Then there are situations like "out of memory" or "disk full"
           | are somewhere in between. All the operands to the calculation
           | exist and are well-defined, the operation is correct, but
           | just there is a resource issue. In pure computing theory,
           | these situations are the heart of the distinction between
           | simulating a Turing machine by means of a finite tape, and
           | the Turing machine abstraction, as such, which has unlimited
           | tape.
           | 
           | In summary, there are basically three categories: errors
           | (programming mistakes), environmental situations (issues in
           | presentation of the inputs to the program, like asking it to
           | operate on a file that doesn't exist, or connect to a host
           | that is down) and resource exhaustions (out of some type of
           | storage).
           | 
           | There is no emotional attachment in this classification
           | whatsoever; and there is value in their separation.
           | 
           | Then there is another category of errors: faults in the
           | hardware. The foregoing assumes that there are no
           | malfunctions in the hardware; no cosmic rays that collide
           | with silicon, flipping the values of bits and such. In some
           | situations, you need a demonstrated strategy for these. Like
           | what if an error happens in a DRAM chip that is not detected
           | and corrected.
        
           | hacker_9 wrote:
           | The issue is we have to pre bake decisions into the
           | application, and until the application is literally an AI
           | this is as good as it gets. When building a program 'errors'
           | occur all the time, but we make design decisions to handle
           | them because we can identify the right path forward in the
           | given context.
        
           | Kaze404 wrote:
           | Isn't that what the Result type on Rust is? Sure, one of the
           | branches is still called Error but it's just a plain language
           | construct (a sum type you can write yourself).
        
             | ymbeld wrote:
             | The Result type is specifically designed to store value-or-
             | error. One may use it diffently but that's what it's made
             | for.
             | 
             | The library designers had a choice between making a generic
             | this-or-that type or a value-or-error type and they chose
             | the latter because they thought that that would be the
             | common this-or-that use-case.
             | 
             | Even Haskell's more generic-sounding "Either" type is made
             | for the same purpose: the "right" (as in correct) variant
             | is the value while the left side is the error, by
             | convention.
        
               | Kaze404 wrote:
               | I don't understand what the problem is, in that case. Is
               | it just the fact that it's called an Error instead of
               | something more generic? Not trying to sound dismissive,
               | just trying to understand if there's something I'm
               | missing.
        
               | ymbeld wrote:
               | The _problem_? You would have to ask the poster that you
               | initially replied to.
        
               | Kaze404 wrote:
               | Fair enough :)
        
               | [deleted]
        
             | creata wrote:
             | Yep, although I guess the Try trait[0] and the
             | corresponding ? operator count as a special error handling
             | language construct.
             | 
             | [0]: https://doc.rust-lang.org/std/ops/trait.Try.html
        
           | [deleted]
        
           | simiones wrote:
           | Well, what you're saying is exactly the reason why java
           | called them Exceptions and not Errors (well, Error also
           | exists, but it is generally reserved for very nasty
           | problems).
           | 
           | Anyway, you can't just define the problem away, or else you
           | end up with Go style error handling - that is exactly what a
           | language with no built-in support for errors looks like.
           | 
           | Languages need to offer control flow mechanisms that allow
           | you to separate common cases in the code from the uncommon
           | cases. Especially since for most code, the uncommon cases
           | appear deep in the bowels of an application, and the only way
           | to handle them is to ask an actual human to handle them,
           | usually on the opposite side of the application stack.
           | 
           | For a more reified implementation of this pattern, the Common
           | Lisp Condition system is very interesting. It is similar to
           | most Exception systems (code can raise a Condition, at which
           | point a handler for that Condition is searched for up the
           | call stack), but with one crucial difference: when raising a
           | Condition, you can also specify Restarts - alternatives for
           | how to proceed. Condition handlers can choose whether to
           | cancel the execution flow and handle the condition with their
           | own logic, OR they can choose to invoke one of the Restarts
           | offered along the condition, based on their own logic. An
           | example would be something like: Raise ConfigFileNotFound;
           | Restarts: ContinueWithDefaults, Retry,
           | ContinueWithOtherFile(newFileName). Then, a Condition handler
           | (possibly a User) can choose to stop the application, or it
           | can choose to continue with the defaults, or it can create
           | the file with its own defaults and Retry, or it can try a
           | different config file path.
           | 
           | This offers a lot of flexibility and has uses outside the
           | idea of handling errors.
        
             | enriquto wrote:
             | > Languages need to offer control flow mechanisms that
             | allow you to separate common cases in the code from the
             | uncommon cases.
             | 
             | Strong disagree with this sentence. It represents the exact
             | opposite of what I deem a good programming language. The
             | difference between "common" and "uncommon" execution paths
             | must be of no bearing. Moreover, this "likeliness" depends
             | on the (unknown to the programmer) usage that the program
             | will be put through. For all you know, the uncommon path
             | may be the only one that will be ever traversed in all
             | instances of your program. A correctly specified program
             | must deal with all possible input conditions, and that
             | includes missing files, inconsistent parameters, and lack
             | of resources. A filename existing or not is the same
             | boolean value as an integer being even or odd.
        
               | creata wrote:
               | > The difference between "common" and "uncommon"
               | execution paths must be of no bearing.
               | 
               | Why? I think that a good programming language should help
               | readers focus on the "essence" of an algorithm. Usually,
               | handling stuff like memory allocation errors is just
               | noise in that effort.
               | 
               | Besides, most errors will usually be propagated to the
               | caller, so I think the programming language should make
               | that convenient, like Rust does with its ? syntax, and
               | most languages do (perhaps _too_ implicitly) with some
               | concept of exceptions.
        
               | junke wrote:
               | Strongly disagree with your disagreement. Programming
               | features help you organize your code. The fact that you
               | actually have a robust software or not with what the
               | language offers is another problem.
               | 
               | When people say "errors are values", I say: yes, but they
               | are a special kind of values. The same way floats are
               | special (signaling NaNs), or that bools are special
               | (short-circuit operators). It is great to have special
               | support for values which need to be used in a certain way
               | (here: don't lose any error, bubble up by default).
               | 
               | It is great to have the opportunity (not the obligation)
               | to use features for separation of concerns. This makes it
               | a bit like an aspect-oriented approach where normal code
               | emits errors and error handling code elsewhere knows how
               | to handle/restart them. If you language allows it, it
               | makes also sense also to decouple logging (tracing
               | functions) or data-access permissions (postgresql row
               | level security).
               | 
               | In the C code I maintain at work, everything is here in
               | plain view, logging, error handling, etc., and I am not
               | complaining, but more recently we tend to use other
               | languages or code generation a lot (think of something
               | like protobuf) because it makes code a lot easier to
               | maintain.
        
               | enriquto wrote:
               | Thanks for explaining so clearly your view. I'm totally
               | biased towards programming practices that make execution
               | paths 100% explicit and local. Yet it is interesting to
               | understand the other point of view (and somewhat
               | liberating, to be honest).
        
               | junke wrote:
               | Thanks for the kind remarks (we are all biased :))
        
               | simiones wrote:
               | Code is primarily meant to be read by humans. Humans
               | can't focus on 30 things at once when reading code. A
               | function that should take a list of strings and return a
               | list of all the strings in the first list starting with
               | 'A' will be harder to read if it must also handle
               | allocation errors for the new list, because they are a
               | completely different kind of concern.
               | 
               | Even outside of programming, human thought often works
               | exactly in terms of general cases and exceptions. It's
               | just what comes naturally, and we shouldn't be fighting
               | it in code.
               | 
               | > The difference between "common" and "uncommon"
               | execution paths must be of no bearing. Moreover, this
               | "likeliness" depends on the (unknown to the programmer)
               | usage that the program will be put through. For all you
               | know, the uncommon path may be the only one that will be
               | ever traversed in all instances of your program.
               | 
               | As the designer of that program, I obviously know up to a
               | very good degree of confidence what the usage of the
               | program will be: I am designing my program for a
               | particular use, by definition. Sure, I may not know
               | exactly how flaky your network may be, but I know that
               | this program only really works on networks that deliver
               | more packets than they drop, that can read all of the
               | bytes I wrote to disk back, at least most of the time,
               | and so on.
               | 
               | Or, perhaps I'm writing a program that is meant to run on
               | very flaky networks (say, a device for wireless
               | thunderstorm sensors): I expect that program to look very
               | different from the Netflix app's networking code.
        
               | enriquto wrote:
               | > I am designing my program for a particular use, by
               | definition.
               | 
               | No; this is bad engineering. You write a program to
               | conform to a specification. In the specification, it says
               | what must happen when a file does not exist, what must
               | happen when there's not enough memory, etc. Then you
               | write the specified behavior into code.
        
               | simiones wrote:
               | What makes the specification superior in principle to the
               | program?
               | 
               | For some domains, you can write a specification that has
               | value in itself, it will be shorter and easier to review.
               | 
               | For other domains, the program itself is the best
               | specification for the desired behavior. There is nothing
               | special about a specification that makes it inherently
               | more correct than a program. The specification may be
               | large enough and detailed enough that it is essentially
               | just as hard to review as the program output. The
               | specification can have subtle bugs, just like a program.
               | Even worse, the specification itself is harder to test,
               | especially for corner cases.
               | 
               | It can be very easy to produce an excellent specification
               | that solves a different problem than expected, especially
               | for complex problems with many moving parts and many
               | possible use-cases.
        
               | kazinator wrote:
               | You're forgetting about errors that happen due to
               | programming mistakes.
               | 
               | I've never seen an end-user application accompanied by a
               | written a specification about what happens if there is a
               | run-time error due to a mistake in the program.
               | 
               | Your original thesis is something like that "there are
               | not errors, only conditions we care about"; but then this
               | subsequent argument is disappointingly about
               | environmental conditions only.
               | 
               | If I have a program that takes two run-time inputs and
               | divides them, I may or may not specify what happens if
               | the denominator is zero. I could explicitly specify that
               | the behavior is not defined; my program can do anything.
               | (In practice, any decent processor will catch it via some
               | numeric exception.)
               | 
               | If my program generates a division by zero due to a
               | programming bug, where no such division is implied by the
               | specification of what the program does on the inputs
               | which it is given, that's something we don't bother
               | specifying. Not usually.
               | 
               | Anything can happen if the program has a mistake. A
               | document listing all possible mistakes and how the
               | program will react will not only be intractably long, but
               | in the course of writing it, you would just inspect that
               | the program doesn't have those mistakes. In the end,
               | you'd be left with a document describing mistakes that
               | the program doesn't actually contain, while it remains
               | vulnerable to unknown mistakes.
               | 
               | We might be required to have a strategy for the software
               | to deal with its own faults in a general way, if we are
               | working on something safety-critical. Such as that no
               | matter how the program might fail, the system as a whole
               | will revert to a safe state.
        
               | jdmichal wrote:
               | You're picking nits that don't mean anything. If a
               | specification exists outside of the definition of the
               | program -- aka the code -- then it is only useful as a
               | point of comparison between intent and implementation.
               | 
               | Most software in business is written to implement a
               | process. And business processes are exactly defined as a
               | common workflow with edge cases and exception handling.
               | Not all software is like this, but a hell of a lot of it
               | is. And those business processes are not always complete,
               | and they are typically changing over time as well. Any
               | idea of a global specification outside of the process
               | that the code is actually implementing becomes useless
               | pretty fast.
        
               | junke wrote:
               | The specification and boundary conditions directly comes
               | from an analysis of the expected usage. Except for some
               | all-purpose libraries you don't develop in a vacuum
               | (assuming you don't work for Roomba).
        
               | enriquto wrote:
               | But design and coding are different steps, best kept
               | separate. When you are designing, I agree with you, the
               | expected usage is very important. But in the design step
               | the particular language mechanism for dealing with
               | conditions does not matter. Once you get a specification
               | to program to, all input conditions can be treated as
               | equal. That is, unless you need to optimize heavily by
               | biasing your execution path for a certain percentage of
               | input cases (which should be clearly described in the
               | specification).
        
               | junke wrote:
               | > Once you get a specification to program to, all input
               | conditions can be treated as equal.
               | 
               | And if the spec says "try downloading the file 3 times at
               | 5 seconds interval; if that fails, give up the update", I
               | am free to implement it however I want (?) unless I
               | missing your point.
        
               | enriquto wrote:
               | sure! for example, in your case you only need a for loop
               | and an if/else statement
        
               | coderdd wrote:
               | Ideally yes, but most of the time I find I have no idea
               | about what I want to do. Idea leads to code, code reveals
               | constraints, those lead to other ideas. If we could
               | specify things, code writing AI would be easy. But most
               | of the time, we just have no idea.
        
               | adwn wrote:
               | > _In the specification, it says what must happen when a
               | file does not exist, what must happen when there 's not
               | enough memory, etc. Then you write the specified behavior
               | into code._
               | 
               | I've never, ever seen a specification which mentions
               | file-not-found or out-of-memory errors. The
               | "specifications" I get from my clients are more like
               | (paraphrased):
               | 
               | > _Listen, adwn, I have all those files in a shitty,
               | undocumented file format designed by another group in
               | another building, could you please add an import button
               | for them? Oh, and can I have it by Friday? Thanks, you
               | 're the best!_
               | 
               | I'm exaggerating only slightly. And then I go and try to
               | make sense of what they need, and I implement error
               | handling on top of it.
               | 
               | Your specifications tell you exactly how to deal with
               | file-not-found situations? You lucky bastard ;-)
        
             | chrismorgan wrote:
             | Rust actually used to use conditions for its I/O errors,
             | but they were removed as part of the "new runtime" project
             | in 2013, for reasons that are dimmed in my memory by time,
             | but I think they included: quite a lot of complexity,
             | including in ways that had negative performance
             | implications; lack of clarity about where errors could
             | occur; lack of ability to distinguish between errors by
             | source in the handler; more limited potential than hoped in
             | ways that were probably connected to Rust's ownership
             | orientation; unfamiliarity to users (and Rust had already
             | spent its weirdness budget); and that the actual power of
             | conditions was almost entirely unused (I honestly don't
             | think I saw any production-like code use handlers for
             | anything interesting, _ever_ ).
             | 
             | You can still readily express the concept of conditions in
             | Rust, but it's no longer baked into the standard library.
        
           | [deleted]
        
           | golergka wrote:
           | > There are no errors; only conditions that you dislike.
           | 
           | This is true in mathematical sense, but unhelpful in UX
           | sense.
        
             | Ace17 wrote:
             | Quite the opposite IMHO : when your program interacts with
             | a user, you cannot panic the program each time something
             | unexpected happens. Here are some examples of unexpected
             | conditions:
             | 
             | - "Null pointer dereference"
             | 
             | - "Out of memory"
             | 
             | - "Disk is full"
             | 
             | - "File does not exist"
             | 
             | - "File does not exist in cache"
             | 
             | - "File exists but is corrupt"
             | 
             | - "Access denied"
             | 
             | - "Connection reset by peer"
             | 
             | It's pretty obvious that all of the above is generally
             | unwanted most of the time.
             | 
             | However, putting them all in the same bag labeled "error",
             | and forcing them to be treated the same way might be
             | counterproductive. Sometimes you might want to panic.
             | Sometimes you might want to retry. Sometimes you might want
             | to ignore!
             | 
             | Now, if your program isn't interactive (such as a
             | compiler), halting on any error might be a choice. But you
             | still have to provide contextualized and accurate error
             | messages, which is easy for the case "File does not exist",
             | and a lot less easy for the case "Out of range index".
        
         | tus89 wrote:
         | > Any error system that relies upon developer discipline will
         | fail because errors will be missed.
         | 
         | Haven't there been some languages that force functions to
         | return some kind of tuple like:
         | 
         | result,error
         | 
         | And _forces_ the programmer to at least do:
         | 
         | if(error) { }
         | 
         | It does not force any kind of correct handling, but simply
         | oversights should be caught. I might be imagining things
         | though.
        
           | simias wrote:
           | That's Go. IMO Rust's approach is vastly saner, since a
           | Result<> type _has_ to be explicitly handled one way or an
           | other. You simply can 't access the returned value without
           | unwrapping it.
           | 
           | Of course that leaves function that can fail but don't return
           | any value, but since Result is tagged "must_use" you get a
           | compiler warning if you don't explicitly discard the result
           | with something like `let _ = foo()`.
        
           | kelnos wrote:
           | There's no "forcing" there. You can simply ignore the error
           | part of the tuple, or even just forget to check it.
           | 
           | If the function returns a (non-error) value that is directly
           | accessible from the function call, the programmer is not
           | forced to do anything with the error.
           | 
           | This is exactly the same problem with null: forget a null
           | check, and you're hosed. Forget an error check, and you're
           | hosed.
           | 
           | If you make errors a part of the actual single return _type_
           | (as Rust does), then you have to explicitly deal with the
           | possibility of an error before you are even allowed to touch
           | the successful case.
        
           | masklinn wrote:
           | What you're describing is Go, except its requirements are
           | much weaker than that.
           | 
           | Rust, meanwhile, is _way stricter_ than that, and has gone
           | way further on the  "not relying on developer discipline"
           | path: a failing Rust function will return a `Result<Value,
           | Error>`. You can't even access the value without explicitly
           | checking whether it's a value or an error one way or the
           | other, which you can very much do so in e.g. Go. Rust also
           | has an attribute called `must_use`, which can be set on types
           | and functions. That attribute causes the compiler to emit a
           | warning if the marked object is not used at all (either the
           | type or the function's result), so while go will not say
           | anything if you write                   Foo()
           | 
           | and that returns an error (or an error and a result you
           | happened not to care for), Rust will absolutely complain by
           | default in the same case, you will need to write _at the very
           | least_                   let _ = Foo();
           | 
           | Go has a second issue, which is that in
           | result, error := Foo()
           | 
           | that you "have to" handle the error is a consequence of the
           | unused variable check (can't define a variable and never read
           | from it). However because it's that instead of something
           | dedicated, this:                   id, error := Foo()
           | result, error := Bar(id)         if error != nil {}
           | 
           | will work fine, with no complaint. Despite possibly passing
           | complete nonsense to Bar if Foo is in error. Also works with
           | id, error := Foo()         if error != nil {}         result,
           | error := Bar(id)
           | 
           | Funnily enough Rust will also warn in both those cases,
           | because it tries to track individual writes.
        
           | TacticalCoder wrote:
           | GP mentionned "maybes". In Haskell (and others) you have for
           | example both maybe and either. When you've got an either you
           | can have either (ah!) the left to indicate an error (and
           | which error) or the right to hold the correct value. And the
           | type system forces you to at deal with both cases (like a
           | maybe forces you to deal with the case where it's "maybe
           | not").
        
         | chmod775 wrote:
         | > Error handling has been wrong since the beginning, and has
         | continued to be wrong ever since.
         | 
         | Maybe it's not error-handling that is the problem then, but
         | undisciplined software developers writing bad code?
         | 
         | Out of all the engineering disciplines, us software engineers
         | have to be the worst bunch by far.
        
           | nx7487 wrote:
           | Yeah, my question is, what's the best direction to go in:
           | 
           | 1. Continue trying to create error-handling systems for devs
           | 2. Try to solve this with more generic static (or dynamic)
           | analysis tools.
           | 
           | I mean, maybe what we need to do is make simulator-esque
           | testing more popular, or something.
        
         | higerordermap wrote:
         | Using exceptions for control flow vs using control flow for
         | exceptions. Who will win?
        
         | MaxBarraclough wrote:
         | > Then we had exceptions, which solved the problem of people
         | forgetting to check by crashing the app.
         | 
         | They can also harm readability, as just about every statement
         | can now cause an early return from a block. They also greatly
         | complicate static analysis.
         | 
         | No discussion of the shortcomings of conventional exceptions
         | would be complete without these two excellent blog posts by the
         | great Raymond Chen:
         | 
         |  _Cleaner, more elegant, and harder to recognize_
         | https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36...
         | 
         |  _Cleaner, more elegant, and wrong_
         | https://devblogs.microsoft.com/oldnewthing/20040422-00/?p=39...
         | 
         | > Attempting to get the complete set of error types that any
         | given call may raise is a fool's errand because of the halting
         | problem it eventually invokes. Forcing people to provide such a
         | list results in the Java problem for the same reason.
         | 
         | The Java problem is one of ergonomics -- unwieldy lists of
         | exception types -- not a computability problem. What do you
         | mean here?
        
         | heavenlyblue wrote:
         | > Attempting to get the complete set of error types that any
         | given call may raise is a fool's errand because of the halting
         | problem it eventually invokes.
         | 
         | That's not true.
        
           | im3w1l wrote:
           | Yeah, it only becomes an issue if you want the compiler to
           | infer that ImpossibleError cannot be raised in
           | while True: pass         raise ImpossibleError
           | 
           | But normally we are perfectly content to put ImpossibleError
           | in the signature for code like this.
        
             | heavenlyblue wrote:
             | Actually in this specific case you can infer that, and you
             | can have an exhaustive list of language constructs beyond
             | which this becomes undecidable.
             | 
             | There is a practical example of "panic!" in Rust to go
             | around that as well.
        
         | brazzy wrote:
         | The article isn't really about any of that though. It's about a
         | much simpler problem: how to produce error output that is
         | useful to developers looking to fix a bug.
         | 
         | And I'm no Rust developer, but it looks to me like it basically
         | demonstrates how Rust is an abject failure in that regard. The
         | developer has to jump through lots of nonobvious hoops and
         | choose between competing libraries to get anything useful.
         | 
         | Contrast that with Java, where any uncaught or logged exception
         | prints a detailed stack trace and (usually) one or more useful
         | error messages, by default, since day one.
         | 
         | This is a problem with very good known solutions, yet Rust
         | seems to fail hard.
        
           | ymbeld wrote:
           | It took Java until Java SE 14 (last year) to produce NPE
           | stacktraces which actually tell you which variable was null.
           | At least in Rust third parties could have created a library
           | to remedy a similar situation.
        
             | jdmichal wrote:
             | For whatever reason, it look a long time for parameters and
             | locals to have a name in addition to a slot and a type in
             | the JVM bytecode. Before that data was introduced, it was
             | an impossible task. Also why things like de-/serialization
             | frameworks required annotations on parameters duplicating
             | the parameter name.
        
           | h0l0gr4ph1c wrote:
           | > And I'm no Rust developer, but it looks to me like it
           | basically demonstrates how Rust is an abject failure in that
           | regard. The developer has to jump through lots of nonobvious
           | hoops and choose between competing libraries to get anything
           | useful.
           | 
           | As a rust developer, there are other ways to skin a cat, or
           | an Error return as it were. For the std::net library (I've
           | been using it when dealing with tcp/udp sockets), when a
           | socket returns an Error, it uses std::io::Error [1]. This is
           | basically a struct that implements the display and Error
           | traits, it stores an enum inside of the types of errors that
           | you may want / need to ignore/ handle. You don't technically
           | need to use libs to do anything error related.
           | 
           | This code has been round since Rust 1.0. Imho, this is kinda
           | how Rust should tell people to make/handle errors. I like it.
           | But I also have found rust enums to be extremely powerful in
           | a lot of use cases.
           | 
           | [1] https://doc.rust-lang.org/std/io/struct.Error.html
        
           | kibwen wrote:
           | _> Contrast that with Java, where any uncaught or logged
           | exception prints a detailed stack trace and (usually) one or
           | more useful error messages, by default, since day one._
           | 
           | I think you're misunderstanding what the OP is trying to show
           | here. If all you want is a backtrace, then Rust supports that
           | out of the box. Here's an approximation of the full error
           | message that you would see from the first example in the
           | post:                   thread 'main' panicked at 'dumping
           | failed', src/main.rs:2:5         note: run with
           | `RUST_BACKTRACE=1` environment variable to display a
           | backtrace
           | 
           | The OP has omitted the next line, which tells you how to
           | receive a backtrace. Backtraces are off by default in Rust
           | because, unlike Java, Rust has a lightweight, C-style runtime
           | that tries not to impose any runtime cost that the programmer
           | did not ask for.
        
             | barrkel wrote:
             | In other compiled languages, stack traces can be supported
             | by looking up code references on the stack in a map of
             | executable addresses to source code locations.
             | 
             | Even in the absence of stack frames, the mere contents of
             | the stack with lookups where possible is really useful, and
             | usually more than enough.
             | 
             | The cost is a little code to do lookups at runtime, and
             | making the mapping data available (typically compressed and
             | embedded in the executable).
             | 
             | (Note that this isn't stack unwinding or exceptions. This
             | is backtraces. Rust, like most languages which embed errors
             | in return values, makes the developer do the stack
             | unwinding manually.)
        
               | edapa wrote:
               | That's exactly how rust generates backtraces though. They
               | just are not typically attached to errors.
        
             | ymbeld wrote:
             | I don't get why this is off by default for debug builds
             | though. Maybe that's a separate concern.
        
               | alpaca128 wrote:
               | In my experience enabling the backtrace is necessary in
               | very few cases. I've defined a bash alias to save me a
               | little typing in those situations. As it can be turned on
               | persistently by setting a single environment variable I'd
               | say it's a good middle ground.
        
           | jdmichal wrote:
           | I'd like to just point out that Java didn't have exception
           | chaining until 1.4. And suppressed exceptions were added in
           | 1.7 along with the try-with-resources construct. I know 1.4
           | was a long time ago, but it was 4 years after 1.2, which is
           | what I typically consider the start of Java becoming a
           | dominant language.
           | 
           | I'm only posting this because I find a lot of people
           | forgetting that Java has had a very long history at this
           | point. And that many of the things we take for granted in the
           | language today did not always exist. I still remember when
           | generics were released. Get off my lawn.
        
             | brazzy wrote:
             | Oh, I've been around since the 1.2 days as well. Somehow,
             | exception chaining didn't register as a big change when it
             | happened. But yeah, incremental improvements have been
             | pretty important as well.
        
               | jdmichal wrote:
               | I agree regarding the chaining. I don't even remember
               | thinking twice about it when it was introduced. But the
               | ability to transform exception types without losing the
               | original context and stack trace is actually a pretty big
               | deal!
        
         | why_Mr_Anderson wrote:
         | Because all systems try to handle several kinds of unexpected
         | behavior in the exactly the same way - invalid arguments (e.g.
         | 'x == null') - code logic (e.g. 'if (x.salary < 0) ...') -
         | external errors (e.g. 'out of memory')
         | 
         | I'd argue that only the 3rd kind is actually 'exception', it's
         | completely out of program's control.
         | 
         |  _Code contracts_ are wonderful way of dealing with 1st and 2nd
         | kind, sadly they didn 't catch up and remains mostly unknown.
         | They are vastly superior to tests and also serve as much better
         | way to document _intent_ of the code, how the author expected
         | the code to work.
        
           | nerdponx wrote:
           | Unless you have dependent types, isn't a contract just
           | syntactic sugar for an if/else that either raises and
           | exception or returns an error code?
        
         | dilap wrote:
         | Are you familiar with Zig's error handling? Except for the fact
         | that errors cannot contain payloads, it is, imo, perfect.
        
           | malcolmstill wrote:
           | Of all the absolutely fantastic stuff that Zig gets right, I
           | think my favourite is the error handling.
           | 
           | While it would be cool to get an error payload, I would hope
           | that if that was added to the language, that it doesn't
           | affect the current ergonomics of error handling.
           | 
           | Where I've really needed to get some data back out, I've
           | passed in a pointer to a struct that gets populated.
        
           | jiehong wrote:
           | it has the same issues. You can always discard errors with
           | `catch unreachable` for example.
        
             | defen wrote:
             | If you haven't overridden the default panic handler, and
             | you're in a Debug or ReleaseSafe build you'll still
             | automatically get an error return trace (which is nicer
             | than a stack trace) even if you do `catch unreachable`.
        
             | dilap wrote:
             | True, but it's _very_ explicit, and easy to audit for.
        
         | randyrand wrote:
         | i've use Java C++ and Objective-C and by far the best approach
         | to errors has been Objective-C's NSError* out parameters.
         | 
         | They're strings. They're chain-able. They're visible. They're
         | hard to ignore. They don't propagate or crash unless you want
         | them to.
        
         | dataflow wrote:
         | I feel like error handling is fine in like C#. No checked
         | exceptions, and stack traces come with the exceptions, and you
         | can nest them. What's not to like?
        
           | kelnos wrote:
           | IMO exceptions are a terrible way to signal errors. They
           | obscure control flow and make it much harder to reason about
           | a program's behavior. They make it easy to forget to check
           | for errors, and encourage sloppy catch-all error handling. It
           | is a lot harder to determine if exception-based code is
           | correct or not, and languages that have exceptions require
           | you to examine literally every line of code to determine if
           | it can throw.
           | 
           | Even the _name_ is wrong: an  "exception" should signal truly
           | exceptional behavior, things that you would not expect to
           | happen unless something is really wrong. And yet exceptions
           | are thrown for mundane things like "file not found",
           | something you could very easily expect to happen routinely.
           | 
           | Exceptions should be like Rust's `panic!()`: only for things
           | that the programmer can't reasonably do anything about, which
           | will cause the program to terminate.
        
             | dataflow wrote:
             | I don't agree with your first or third paragraphs but I do
             | kind of agree with the second, yet I think the solution to
             | that is to provide return-value-based solutions where it
             | makes sense, not to avoid exceptions in general.
             | 
             | Also, things like "user canceled this operation" are great
             | for exceptions IMO... exactly how would you stop (say) a
             | sort() function otherwise? You'd need to write a custom
             | cancelable sort, which isn't a great idea? You can't
             | reinvent the wheel for everything.
        
         | samatman wrote:
         | > _It 's a hard problem, which is why no one has solved it
         | yet._
         | 
         | On the contrary, the Common Lisp condition/restart system
         | solves it, and it's maddening that this hasn't been adopted
         | anywhere else.
         | 
         | An exceptional state signals a condition, and _without
         | unwinding the stack_ , looks for a handler for that condition,
         | which can, among other things, restart the computation from a
         | lower stack frame. In development mode, it defaults to dropping
         | into the debugger/repl; in release mode, an unhandled condition
         | panics.
        
         | dnautics wrote:
         | zig gets this pretty close to "right" as per your definition.
         | 
         | > Any error system that relies upon developer discipline will
         | fail because errors will be missed.
         | 
         | You must handle all errors on egress to either C abi or a
         | function that does not have an error signature.
         | 
         | > Any error system that handles all errors the same way will
         | fail because there are some errors we can ignore, and some
         | errors we must not ignore. And what's ignorable/retriable to
         | one project is not ignorable/retriable to another.
         | 
         | trys, which are the "lazy" way of error handling (not counting
         | "catch unreachable" - which promotes errors to panics and
         | shouldn't be used except in dev) automatically append the error
         | return code to the trying function's error return call.
         | 
         | > Attempting to get the complete set of error types that any
         | given call may raise is a fool's errand because of the halting
         | problem it eventually invokes. Forcing people to provide such a
         | list results in the Java problem for the same reason.
         | 
         | If every function has a well-defined tree of possible internal
         | "call dependencies" that has finite set of type signatures, you
         | don't have this problem.
        
         | mrmr1993 wrote:
         | I think that exceptions are a problem and cause this developer
         | burden only because they are invisible. If they appeared in the
         | type signature, for example as
         | 
         | () -[DatabaseReadError]-> ()
         | 
         | then they would be part of a function's 'contract'.
         | 
         | With this, consumers of your function are making an active
         | decision about whether to handle or bubble an exception without
         | examining your implementation, and the type of the main
         | function tells you which errors will end up being fatal.
         | 
         | Disclaimer: this is not a new idea. There are proposals for
         | OCaml to add 'algebraic effects', which have similar
         | annotations on arrows, and I believe there have been
         | discussions around using the syntax to track exceptions.
        
           | brazzy wrote:
           | This is exactly Java's checked exceptions, which I'd call a
           | failed experiment in language design.
           | 
           | The problem is that it makes the most common case (letting
           | the exception bubble up) very inconvenient and clutter-y.
           | Signatures with 5 different declared exceptions are worse
           | than useless.
        
           | simiones wrote:
           | The biggest problem with that is that it is very unwieldly if
           | you're using any kind of higher-order functions.
           | 
           | To fix that, you need to start supporting error polymorphism.
           | For example, `map` should have a signature like
           | map :: List a -> (a -> b -[err]) -> List b -[err]
           | 
           | So that                 map [1 2 3] +1 //no errors       map
           | [1 2 3] sendOnNetwork //returns NetworkError
           | 
           | At least, this is one of the major limitation of Java's
           | Checked Exceptions idea, one which often forces you to use
           | Unchecked Exceptions.
           | 
           | Another common problem is handling exceptions which occur
           | while handling another exception. D has the best default I've
           | seen in that area (it will automatically collect all of the
           | errors, instead of panic()-ing like C++ or discarding the old
           | exception like Java and C#).
        
             | junke wrote:
             | And if the function can throw different kinds of
             | exceptions, you need a way to express the union of
             | unrelated exceptions (without defining a new variant type),
             | a bit like Polymorphic Variants I guess.
        
               | dllthomas wrote:
               | Ideally also a way of saying "... except for X Y Z" while
               | remaining appropriately polymorphic. I've remarked before
               | that while checked exceptions help make sure you consider
               | every possible situation, imprecision forces you to
               | consider many (too many?) impossible situations as well.
        
             | dasyatidprime wrote:
             | There've been a few times I've used checked exceptions very
             | locally in Java where I did use a type parameter
             | successfully for this--though it's not really threaded
             | through the type signatures in the standard libraries, so
             | interop can be a problem. Almost like what you wrote, of
             | course more verbosely, something like:
             | interface SomeProcessor<E> {             void process(Thing
             | thing) throws E;         }              <E> void
             | processThingsSomehow(             SomeProcessor<E>
             | processor,             Container<Thing> things,
             | Parameter how) throws E {             // ... {
             | processor.process(extractedThing);             // } ...
             | }
             | 
             | and then later:                   void processOrFail(Thing
             | thing)             throws SomeCheckedException {
             | // ...         }              void processOurThings() {
             | try {                 someObject.processThingsSomehow(
             | this::processOrFail,                     getThings(), HOW);
             | } catch (SomeCheckedException e) {                 // ...
             | }         }
             | 
             | and it definitely worked the way I expected--if the correct
             | exceptions weren't caught in processOurThings, it wouldn't
             | compile, and processThingsSomehow did not have to catch
             | them. It even worked in at least some cases with multiple
             | throws on the concrete SomeProcessor, though I think the
             | different exceptions involved had an upper type bound
             | within the package; I don't know how well that's handled in
             | the general case.
        
               | simiones wrote:
               | Nice, I never actually tried to do this (my problem was
               | more that I needed to use built-in functions that don't
               | throw, for example sorting a list with a Comparator which
               | can throw).
        
         | brundolf wrote:
         | It's not perfect, but I think Rust's approach is the best one
         | yet.
         | 
         | > Any error system that handles all errors the same way will
         | fail because there are some errors we can ignore, and some
         | errors we must not ignore.
         | 
         | Rust has separate categories for these two things. Panics
         | _cannot be handled_ , which pushes the author to use them
         | sparingly. Results _must be handled_ (or explicitly elevated to
         | panics, in a way that 's easy to track down later).
         | 
         | The main weakness of this system, imo, is that the thrower, not
         | the caller, decides whether or not a given error must be
         | handled, and sometimes the answer is "it depends on what the
         | consumer is doing".
         | 
         | That said, the powerful (mainly, no-implicit-null...) type
         | systems that are starting to become prevalent prevent a lot of
         | cases that would have been errors 20 years ago from existing in
         | the first place. In modern typed languages an error is almost
         | always a genuine IO/environment anomaly, which means there are
         | a lot fewer of them to handle in the first place.
         | 
         | Edit: The other big weakness of Rust's system, as mentioned in
         | the article, is the jumble of all the different disjoint Error
         | types. I think the core issue here is that Rust doesn't have
         | ad-hoc union types. I.e. if you want to say "this value is of
         | type X or Y or Z", you have to declare a new enum somewhere,
         | and embed your possible types in each branch, and that's a lot
         | of ceremony at each point where you bubble up a different
         | possible type of error. I understand why Rust's enums are
         | static, though, I wonder if there's a place for some sort of
         | heap-allocated ("dyn?") solution that allows any combination of
         | possible types.
         | 
         | Edit 2: Actually this probably wouldn't be possible because
         | there would be no way to distinguish the union, because Rust
         | carries no type info at runtime. Maybe there could be syntax
         | sugar for anonymous enum types declared inline?
        
           | mlindner wrote:
           | One of the biggest issue with Rust's panics is there's many
           | times when you must never panic. For example in an OS, when
           | trying to save your crucial data to disk, in real-time code
           | where panicking would maybe kill someone in the real world,
           | etc
        
             | kibwen wrote:
             | I don't really understand this criticism, because there's
             | no good alternative. Every language is capable of producing
             | invalid states that the programmer did not intend; consider
             | `x / user_input()`. (Unless literally _every_ possible
             | invariant of the program is expressed in the type system,
             | which is not something that we have figured out how to do
             | at scale and not something that even the most type-heavy of
             | the popular languages come anywhere close to.) And once you
             | 're in an invalid/unanticipated state, you either crash or
             | you don't. Of the two options, the latter is far worse (On
             | Error Resume Next, anyone?), and is just as likely to lose
             | data/get someone killed.
        
               | masklinn wrote:
               | > I don't really understand this criticism, because
               | there's no good alternative.
               | 
               | There's the option of surfacing the panic-ability of a
               | function in the same way the constness is surfaced, which
               | would allow some subsets of the _code_ to ensure they won
               | 't call a possibly-panicing thing, even at the cost of
               | convenience.
        
           | brazzy wrote:
           | >The main weakness of this system, imo, is that the thrower,
           | not the caller, decides whether or not a given error must be
           | handled, and sometimes the answer is "it depends on what the
           | consumer is doing".
           | 
           | So... exactly the same problem that Java's checked exceptions
           | have?
        
             | [deleted]
        
             | dralley wrote:
             | Except more ergonomic and less verbose, usually.
        
             | masklinn wrote:
             | > So... exactly the same problem that Java's checked
             | exceptions have?
             | 
             | Well yes, but also no because Java's checked exceptions
             | have issues which go way beyond that. Hell I'd say this is
             | not an issue because of the other issues.
             | 
             | In Rust the thrower decides whether the error must be
             | handled, but the default is "yes", and it's the
             | overwhelmingly common decision. Panic is the exception (or
             | multiple APIs are provided). Rust also provides easy way to
             | convert errors to panics, and syntactic sugar to convert
             | between error types.
             | 
             | In Java, the classifications were much less stark, and thus
             | more arbitrary, some exceptions were checked, others were
             | not, but there was little rhythm or reason about it.
             | 
             | Furthermore, there was little to no ability to abstract
             | over checked exception, and the statement-oriented nature
             | of the language made both converting checked to unchecked
             | or converting between different checked exception types a
             | chore.
             | 
             | The verbosity and horrible ergonomics of java's checked
             | exceptions is where the problem always lied, really, with
             | the seemingly arbitrary nature of the classification coming
             | in at third.
        
               | zaphar wrote:
               | Errors are a part of the API. I'm not sure why we keep
               | thinking we need to treat them differently. We don't
               | complain when we have make decisions based on or
               | transform data that is passed to us from a function or
               | method. We just handle the data. Errors are just more
               | data.
               | 
               | The correct thing here is to correctly model your Error
               | domain and map errors you don't control to Errors that
               | you do. In the Java Checked Exception case the Throws
               | clause only grows because everyone is trying to ignore
               | the errors and make somebody else handle it. I would
               | argue that in Rust the explosion of different generic
               | error handling mechanisms is also laziness and a desire
               | to not handle the other half of the API's you are
               | handling.
        
           | caffeine wrote:
           | Also, panics can be caught, and now that ppl write a lot of
           | async code, many panics do get caught by default. So it's
           | just a Java RuntimeException now, buried deep inside the
           | dependency tree layers down..
        
         | jiehong wrote:
         | Would you put the Erlang/Elixir error handling in the "catch
         | Throwable" camp?
         | 
         | Because in this case, the error handling is done in a different
         | process altogether (supervisor).
        
           | Floegipoky wrote:
           | I think of it as "no error handling", the program just
           | crashes. There just happen to be many other programs running,
           | which may restart the program that crashed.
        
         | Ygg2 wrote:
         | > It's a hard problem, which is why no one has solved it yet.
         | 
         | The way you described it seems not so much as hard problem.
         | More like an impossible one.
         | 
         | That said. Perhaps error handling isn't one problem at all, but
         | several problems masquerading as one. In which case Rust
         | approach makes much more sense.
        
         | tasubotadas wrote:
         | Nice summary. Ideally, I would like languages just to do simple
         | exceptions (no checked stuff) and create a way to catch them.
        
         | mhh__ wrote:
         | The only system I think should completely go is Exceptions
         | except in the case of termination or absolutely catastrophic
         | failure - this isn't really about programming but rather that
         | the implementation is a total pain, the compiler struggles to
         | optimize them, and even better they make quite a few safety
         | analyses like borrow checking very difficult because the
         | control flow graph basically explodes when you start
         | considering exceptional control flow - even on a basic block
        
           | simiones wrote:
           | In terms of control flow, this:                 try {
           | someCall();       } catch (ErrorICanHandle err) {
           | //handle       }
           | 
           | perfectly equivalent to this:                 err =
           | someCall();       if canHandle(err) {         //do something
           | } else {         return err       }
           | 
           | I don't see why so many people think exceptions make code
           | harder to analyze. In my opinion, it is _errors themselves_
           | that make code hard to analyze, regardless of implementation
           | strategy.
           | 
           | The only difference between exceptions and error
           | returns/error codes is in human readability: one makes the
           | bubbling behavior explicit, which clutters the code but makes
           | it obvious; the other makes the bubbling behavior implicit,
           | which keeps the code cleaner, but also less obvious.
           | 
           | After writing professionally in both languages with
           | Exceptions (Java/C#/Python) and in Go, I much prefer
           | Exceptions to error values/codes, but I can appreciate that
           | it may be different in other domains or for other people.
        
             | xpressvideoz wrote:
             | I mean, Go's error handling is one of the worst
             | implementations of the "error code" idea, so it is a bit
             | unfair to disregard the idea just because one particular
             | implementation is bad. You should try Rust or Swift, at
             | least the error handling part, to fully appreciate what a
             | good error code implementation could be.
        
               | simiones wrote:
               | I'd love to when I get a chance. I did see that Rust
               | seems to favor a ? macro that seems to make error codes
               | behave essentially like exceptions, so I am curious to
               | try it out at some point and see if that ends up being
               | any different from exceptions in practice.
        
             | BlackFly wrote:
             | Exceptions are harder to analyze because they create scopes
             | from which you are already "handling" errors, but if you
             | add code to the scope that throws a new error of that type
             | your handler may not actually be able to handle it.
             | try {         someCall();         someOtherCall();       }
             | catch (ErrorType err) {         //handle       }
             | 
             | Which method throws? If they both throw ErrorType but one
             | of them cannot be handled, then you have to put additional
             | logic into the catch and rethrow.
             | 
             | This is a common source of faulty error handling.
        
               | simiones wrote:
               | Yes, this is an easier mistake to make with exceptions
               | than with error values. Still, the fix is to do exactly
               | the same thing as in the error values case:
               | try {         someCall()       } catch (ErrorType err) {
               | //handle one way       }       try {
               | someOtherCall()       } catch (ErrorType err) {
               | //handle another way       }
               | 
               | It's no less verbose than the error values way, though
               | again, it is easier to make the mistake in the first
               | place.
               | 
               | I'm not trying to claim that exceptions are ultimately
               | better than error values, just that the difference isn't
               | so much "non-local control flow" as it is explicitness vs
               | implicitness. Exceptions are implicit, error codes are
               | explicit. Both have benefits and drawbacks.
        
             | gmfawcett wrote:
             | Exceptions make code harder to analyze because they
             | implement non-local control flow. Your two example
             | statements aren't really equivalent. `ErrorICanHandle` may
             | be a kind of exception that the `someCall` function has no
             | idea about whatsoever -- it could be an exception
             | propagated from much farther down the call stack -- while
             | your if statement assumes the error was explicitly returned
             | from the function.
        
               | simiones wrote:
               | In an exception-based language, assuming all functions
               | potentially throw, code like this:
               | someCall()
               | 
               | is equivalent to code in a language without exceptions
               | that looks like this:                 err = someCall()
               | if err {         return err       }
               | 
               | (ignoring for a moment non-error returns, which don't
               | change the point significantly)
               | 
               | Having functions that are guaranteed not to throw /
               | return errors doesn't significantly change this shape.
               | 
               | So basically it's no more non-local than the code you'll
               | end up writing with error codes, in most cases, any way.
               | The only difference is implicit vs explicit.
               | 
               | > `ErrorICanHandle` may be a kind of exception that the
               | `someCall` function has no idea about whatsoever -- it
               | could be an exception propagated from much farther down
               | the call stack -- while your if statement assumes the
               | error was explicitly returned from the function.
               | 
               | someCall doesn't need to know about the meaning of the
               | error in either case. It only needs to propagate any
               | error it receives from any functions it calls (that it
               | can't handle itself). Think about the function I wrote
               | myself: did it need to know the precise type of err? Nope
               | - it just needed to know that it implements some kind of
               | Error interface (which may be as simple as being a
               | negative number, like a POSIX error code).
               | 
               | Edit to give a more complete example:
               | extern void bar();       void foo() {         bar()
               | doOtherStuff()       }       void someCall() {
               | foo()         doMoreStuff()       }       void main() {
               | try {           someCall()           doFinalStuff()
               | } catch (BarSpecificException e) {
               | doErrorStuff()         }       }
               | 
               | Is equivalent to this code:                 extern err
               | bar()       err foo() {         err = bar()         if
               | err {           return err         }         err =
               | doOtherStuff()         if err {           return err
               | }       }       void someCall() {         err = foo()
               | if err {           return err         }         err =
               | doMoreStuff()         if err {           return err
               | }       }       err main() {         err = someCall()
               | if err is BarSpecificException  {                 err =
               | doErrorStuff()           if err {              return err
               | }         } else if err {           return err         }
               | err = doFinalStuff()                  if err is
               | BarSpecificException  {                 err =
               | doErrorStuff()           if err {              return err
               | }         } else if err {           return err         }
               | }
               | 
               | If I ask you in the second piece of code what will be
               | executed after the call to foo(), is it any easier to
               | reply than in the first version? Personally, I don't
               | believe so.
        
             | edapa wrote:
             | For those of us who don't like exceptions I think the
             | concern is more that the exception might not be caught
             | right there, so an exception may potentially transfer
             | control to an arbitrary point above the current function in
             | the stack.
        
             | metreo wrote:
             | this issue is knowing whether you are catching the right
             | errors
        
               | simiones wrote:
               | You have the same problem with error codes, don't you?
               | 
               | The bigger difference is knowing whether you should
               | expect errors at all - with exceptions, you can forget to
               | handle an error and you may screw up an important
               | assumption, like not unlocking a Mutex if an Exception is
               | raised. With error codes, you have the opposite problem:
               | you may forget to check for an error, and continue to
               | execute in a bad state.
        
           | skinkestek wrote:
           | > the compiler struggles to optimize them,
           | 
           | Nitpick: I'd say if you need the compiler to optimize
           | exception handling you are using them wrong.
           | 
           | Exceptions are for exceptional circumstances.
           | 
           | (if you mean that exceptions mess up optimization of
           | surrounding code that could be a bigger deal but I won't
           | accept that without pointers to benchmarks$
        
       | EugeneOZ wrote:
       | Error handling in Rust is the most appealing feature for me. Not
       | performance. I really can not understand people who want to
       | ignore error cases or write their code pretending that error will
       | never happen. All of these "unwrap", "expect" sounds to me like
       | "ok, there might be an error but I'm too lazy to write code to
       | handle it so let's just hope it will never happen".
       | 
       | Of course, when your code is full of "panic"s, then errors
       | handling will be a mess, but it just means your code is a mess
       | and you need to finally take care of the errors. 'Result' and
       | 'enum' are the best friends in this.
       | 
       | By writing the code to handle errors you'll find that many of
       | them are recoverable; that some errors might look "critical" for
       | one function and "insignificant" for another, so if you will not
       | panic and just return this error, at some point of the path it
       | will be recovered.
       | 
       | This post mentions some useful libraries for simplifying the
       | errors tracing (with contexts), but the tone is too sarcastic,
       | sounds close to hysterical.
        
       | Subsentient wrote:
       | For the mostpart, Rust error handling is okay. What really
       | rustles my jimmies, however, is the often mandatory indentation
       | because of a lack of an inverse "if let". I prefer to bail out of
       | a block if a condition is NOT met, rather than execute another
       | nested block if it IS met. Rust makes that harder than it should
       | be. It's good code hygiene in every other language, and Rust
       | makes it painful in places. I've even been stopped from doing
       | this by literal bugs in the borrow checker.
        
         | seeekr wrote:
         | Couldn't this "inverse if let" be provided with a simple macro?
         | Is that where you encountered bugs in the borrow checker?
        
         | EugeneOZ wrote:
         | Try methods "is_err", "is_ok"; or for Option: "is_some",
         | "is_none".
        
           | duckerude wrote:
           | That's not great if you still want to use the underlying
           | value, because then you need to unwrap it later.
        
         | nulptr wrote:
         | Yeah, this is irritating... one workaround for this is to do:
         | 
         | (in the context of walking a tree...):                   fn
         | get_depth(root_opt: &Option<Box<TreeNode>>) -> usize {
         | let root = match root_opt {                 Some(root) => root,
         | None => return 0;             };                  1 +
         | std::cmp::max(get_depth(root.borrow().left),
         | get_depth(root.borrow().right))         }
        
         | tiddles wrote:
         | The most common time this comes up for me is trying to use
         | continue/break if a value is None/Err :                 loop {
         | let thing = match foo() {           Some(bar) => bar,
         | None => continue,         }       }
         | 
         | Yet I still always first try to put the continue in an
         | unwrap_or_else first..
        
         | gardaani wrote:
         | I totally agree. I love Swift's guard let. It makes early
         | returning [1] easy:                   guard let value =
         | optvalue else {             return // optvalue is none
         | }
         | 
         | There has been several proposals [2][3] to fix it in Rust but
         | they don't seem to go anywhere.
         | 
         | I'm using this in my own code now to unwrap or return (it looks
         | stupid):                       let value = if let Some(value) =
         | optvalue {                 value             } else { //
         | optvalue is none                 return;             };
         | 
         | [1] https://szymonkrajewski.pl/why-should-you-return-early/
         | 
         | [2] https://github.com/rust-lang/rfcs/issues/2616
         | 
         | [3] https://github.com/rust-lang/rfcs/pull/1303
        
           | danappelxx wrote:
           | I missed `guard let` for a while too, but eventually stumbled
           | upon this pattern which is almost as good:
           | let value = match value {             None => return,
           | Some(value) => value         };
        
         | exrook wrote:
         | I'm confused, have you found the try operator ("?")
         | insufficient for your use cases? I believe it does what you are
         | describing, ex:                   fn process_file(p: Path) ->
         | Result<String, io::Error> {             let file =
         | File::open(p)?; //Return err if file can't be opened
         | let mut out = String::new();
         | file.read_to_string(&mut out)?; // Return err if read fails
         | out         }
         | 
         | If you want to handle the error case within the same function
         | `try` blocks are available in nightly[0] and will eventually
         | come to stable[1]
         | 
         | [0] https://doc.rust-lang.org/nightly/unstable-book/language-
         | fea...
         | 
         | [1] https://github.com/rust-lang/rust/issues/31436
        
           | Arnavion wrote:
           | `?` only helps if the thing you want to do on Err is return
           | from the whole function. You can't use it for finer-grained
           | break / continue / exit-from-current-block (until `try`
           | blocks are stabilized).
        
         | conradludgate wrote:
         | You can do something like that
         | 
         | ``` let x = Some(1); let x = match x { Some(x) => x, None =>
         | return, };
         | 
         | assert_eq!(x, 1); ```
        
           | Zababa wrote:
           | HN doesn't uses Markdown syntax for formatting: "Text after a
           | blank line that is indented by two or more spaces is
           | reproduced verbatim. (This is intended for code.)"
           | https://news.ycombinator.com/formatdoc
        
       | scottlamb wrote:
       | failure [1] is still state-of-the-art, even though it's
       | deprecated. It will get you backtraces (if RUST_BACKTRACE=1 is
       | set), which nothing else will. I'm waiting until there are stable
       | built-in backtraces [2] to switch to something else.
       | 
       | I recently tweaked my application's failure reporting [3] to go
       | from this embarrassing thing:                   E0211 073750.559
       | main moonfire_nvr] Sys(EROFS)
       | 
       | to the more useful:                   E0211 111025.109 main
       | moonfire_nvr] Exiting due to error: Failed to open dir
       | /home/slamb/mymount/sample         caused by: Unable to open meta
       | file         caused by: EROFS: Read-only file system
       | (set environment variable RUST_BACKTRACE=1 to see backtraces)
       | 
       | I should have thought long ago to advertise setting
       | RUST_BACKTRACE=1 right in the error message, but better late than
       | never.
       | 
       | [1] https://crates.io/crates/failure
       | 
       | [2] https://github.com/rust-
       | lang/rust/pull/72981#issuecomment-72...
       | 
       | [3] https://github.com/scottlamb/moonfire-
       | nvr/commit/9a5957d5efa...
        
       ___________________________________________________________________
       (page generated 2021-02-19 23:01 UTC)