[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)