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.
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, ...).
? 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.
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?
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.
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.)
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.
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.
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.
> 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.
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
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.
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.
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.
The "?" operator doesn't turn them into exceptions, it's just a "return-early-if-error" shortcut. The main difference being that the caller still has to handle the error of a function using "?". (even if it's by punting further up the call stack with more "?", which you could argue is exactly how exceptions work, but it is at least explicit in what functions can fail and which don't)
Yeah, I think java had the right idea, but the ergonomics didn't turn out great, so they didn't get the buy in they'd hoped for. Having both checked and unchecked exceptions (ie, NPEs) meant it doesn't offer the same safety guarantees as something like rust does. The implementation of exceptions can also have some implications for the runtime, whereas the enum method of error handling allows you to run in very minimal environments and easily see what's going on. (Exceptions could be implemented this way under the hood, but they often aren't)
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.
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.
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).
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.
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.
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.
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.
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$
> 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.
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.
> 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.
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.
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).
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.
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.
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...
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.
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.
> 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.
> 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.
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.
> 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.
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).
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).
> 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.
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.
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.
> 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 ;-)
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.
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.
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.
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).
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.
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.
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.
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.
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".
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.
I'm confused. Isn't that just C? There's no errors in C, or exceptions. The only exceptions are interrupts/signals, but those aren't part of the language, but the machine or OS.
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.
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#).
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.
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.
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:
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.
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).
> To fix that, you need to start supporting error polymorphism.
> map :: List a -> (a -> b -[err]) -> List b -[err]
FWIW, Haskell has this already; it's called[0] Traversable:
ghci> :t mapA
mapA :: (Traversable t,Applicative f) => (a -> f b) -> t a -> f (t b)
ghci> :t mapA @(Error ||) @[]
mapA :: (a -> Error || b) -> [a] -> Error || [b]
0: The default prelude calls it `traverse` instead of `mapA` (which is terrible for reasons that should be obvious[1]) and `Either Error a` instead of `Error || a` (which is a matter of taste).
1: Especially if you rename the method of Functor from `fmap` to:
map :: Functor t => (a -> b) -> t a -> t b
-- for comparison:
mapA :: ... => (a -> f b) -> t a -> f (t b)
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.
> 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.
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`.
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.
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.
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?
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
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.
> 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.
In the cases where the problem is actually panicing (ie, a non-total function) rather than your choice of core-dumping code, lack of panic-ability is strictly insufficient - `while(1){/*busy-loop*/}` is exactly as bad as `panic();`, and what you actually need is not "must never panic", it's "must make forward progress within some (not necessarily rigorously defined but) small amount of time". That doesn't have anything to do with panic besides that `panic` would be declared as `fn(string)->noreturn[takes_upto: (infinity * seconds)];`, rather than just `fn(string)->noreturn`. (And really, noreturn ought to be enough to infer that on its own.)
> noreturn only works for functions which never return. Most panicking functions do return, just not all the time.
Sorry, I meant that any function that was noreturn could be inferred to take up to forever to return (since it always takes forever to return, since it never returns). Then, any function that calls such a function (in a non-DCEed position) can be inferred to also take up to forever to return (even though it might take less than that), because some possible code path through it (it's up to X amount of time to return) takes forever due calling a function that takes forever.
That is true, although I don't know how helpful that is aside from checking that the annotation is not broken? Much like `const`, the annotation would have to be opt-in either way:
* Rust doesn't infer function types (on purpose)
* like const (or Copy), the removal of the flag is a breaking change, so it should not happen implicitly
So functions would necessarily have to be explicitly marked as e.g. `convergent` or whatever. And while callers of `!`-functions would be easy to check for, I don't think rustc is currently smart enough to infer the totality loops in general.
> I don't think rustc is currently smart enough to infer the totality loops in general.
Well, yes; we're necessarily talking about changes to the language (or, equivalently, possible different languages).
> while callers of `!`-functions would be easy to check for, [other problems wouldn't]
That was my point, thanks: "when trying to save your crucial data to disk, in real-time code where panicking would maybe kill someone in the real world, etc", checking for the easy-to-check-for problems doesn't actually suffice to prevent disasters.
Interesting. The company I just left sells a load balancer/application server/do-everything-box and one of the rules of the coding is that it must never crash. Even out of memory errors need to be recovered from and we shut off the kernel feature that makes malloc not actually malloc memory.
>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?
> 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.
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.
It seems programmers want errors to handle themselves. It's like they want errors to automagically hook themselves into the entire code base and do "the right thing". Or maybe in other words: programmers mistake errors-as-additional-data, with errors-as-automatic-runtime-behaviour.
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..
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.
> 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.
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.
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.)
> 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.
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.
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.
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!
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.
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.
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.
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()`.
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").
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:
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.
> 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.
> 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:
> 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?
The only programming language where exceptions are such a problem is C++.
First of all, because a religious subculture will run to pitchforks and torches at the slight mention of the word exceptions in the room.
Then the overall machinery in place to make them work alongside code that was most likely written as C originally.
And naturally every ms and byte lost to exceptions infrastructure is a capital felony, so even more fine grained semantics to make it happen.
C++ is still the language I reach for when not doing any JVM/.NET language, but things like this is what takes the fun out of using it with another ones on a team.
> The only programming language where exceptions are such a problem is C++.
I'm not sure what you mean here, neither article is about the low-level issues that arise in C++. The second article only discusses C#. Chen's point is more about the way every call can surprise you by causing your code to return early.
You're right that the C language has many weird and wonderful capabilities and footguns. How does that address Chen's point about exceptions being unmanageable?
His point doesn't stand in any programming language that doesn't enforce error checking, exceptions or not, you will be surprised at any possible execution point if you don't check error values from previous call.
At least with exceptions you get a "I told you so" if something does happen, and a defined way to force application termination if the error isn't properly handled.
Given the remarks on the C++ post, I assume he belongs to the /EH- side of pitch forks and torches, which actually goes against the C++ best practices at Microsoft, https://docs.microsoft.com/en-us/cpp/cpp/errors-and-exceptio..., which leaves a feeling of a kind of biased article in some way.
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?
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.
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.
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.
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.
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
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]
`?` 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).
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.
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.
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.
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.
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?
I've used gdb just fine on Rust code. Pretty printing isn't always as good, but if Rust is crashing, I'm probably down towards the syscall level anyways (or blowing the stack size as my last observed crashes were triggering).
I've had similar experiences. Inwas referring to the parent arguing that using Results and error enums to debug your own code is silly when you could just step backwards in a debugger. In my experience the rust debugging story isn't fluid enough that it's better than logging to debug most logic bugs.
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).
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.
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.
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`.
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
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.
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!
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.
In C applications people will generally fprintf(stderr, "Froboz option requires a grimulax!\n");exit(EXIT_FAILURE); Which is pretty useful.
In rust, my experience is that it's far more common to unwrap an empty result and just toss a panic which spams some almost completely inscrutable error at the user.
I'm not sure what the cause is-- it's been suggested to me by a heavy duty rust user that part of it is norms for library code (where exiting-as-error-handling would not be very welcome) and applied them to the whole ecosystem.
I've also been told that it's not uncommon for people to use library code for handling stuff that would be manually written in C but then the library code under-documents what the failure cases are and so the caller can't easily/obviously do anything more than panic on an empty result.
Whatever the cause the end result is that literally every single program I pull off github written in rust I encounter some panic. In fact, as of a about a month ago even firefox started regularly crashing for me w/ some panic in servo (triggered by twitter).
The majority of the panics I get from stuff are just trivial "You used the wrong command-line options", but it's hard to resist the impression that software written in rust is simply a lot lower quality. It might well be that some of this is software that wouldn't even have been published had it been attempted in C, or that it's just because a greater percentage is written by developers who are new to programming (or at least new to systems programming)... but that isn't observable.
The net result though is that it leaves me feeling uneasy and dubious about the claims of rust lowering defect rates, especially since there has been no formal validation of those beliefs. It sounds credible for sure, but if rust is freeing up programmers to worry about things other than memory safety, it's not showing from my experience. You can go back 5 years in my history and see that I promoted rust a lot -- I even opted for my former company to use it. But that was before encountering other people's rust code in practice.
I think for years java suffered greatly from similar bad impressions-- where you don't expect to be able to run something you found without dealing with a maze of inscrutable null pointer exceptions.
Sure, a library has to handle errors differently than an application using that library.
The library should "back off" by aborting anything it is doing and indicating failure without crashing or exiting the process (a return value indicating an error, setting the "errno" variable, making an error pointer provided by the caller point to an error object/struct, ...). Of course, the other side of the coin is that caller (the application or another library) has to actually check for that error indication, and determine what to do about it.
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.
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.
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 :)
> '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.
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.
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.
203 comments
[ 3.4 ms ] story [ 208 ms ] threadlike 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.
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, ...).
E.g. you can have tests that return an anyhow::Result<()>, and use ? anywhere in the test. The test will fail if the result is not Ok(()).
Sure, you have the additional boilerplate of the return type annotation and the final Ok(()), but the test logic itself reads nicer, I think.
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?
Last time I checked, Rust couldn't even catch malloc failures.
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.
This also looks interesting: https://github.com/Technolution/rustig
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.
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.
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.
Yes, this is exactly what I meant.
> it is at least explicit in what functions can fail and which don't
So is Java with exceptions (at least as long as developers are even slightly disciplined).
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.
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:
Is equivalent to this code: 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.This is a common source of faulty error handling.
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.
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$
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.
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.
That’s not true.
There is a practical example of “panic!” in Rust to go around that as well.
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.
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.
[0]: https://doc.rust-lang.org/std/ops/trait.Try.html
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.
So you can also build an enum that doesn't have any explicit mention of which is the right path
Forth?
Worse, it's COME FROM!
https://en.wikipedia.org/wiki/COMEFROM
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...
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.
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.
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.
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.
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.
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.
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.
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 ;-)
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.
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.
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.
You can still readily express the concept of conditions in Rust, but it’s no longer baked into the standard library.
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.
It doesn't solve the issue of forcing programmers to handle errors..but then, CL doesn't care a lot about hand holding.
This is true in mathematical sense, but unhelpful in UX sense.
- "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".
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.
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.
() -[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.
To fix that, you need to start supporting error polymorphism. For example, `map` should have a signature like
So that 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#).
1: Especially if you rename the method of Functor from `fmap` to:
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.
> 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.
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.
Because in this case, the error handling is done in a different process altogether (supervisor).
> 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?
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.
Sure. It is also by far the largest risk you face, so would solve a good 4 or 5 nines if the issue.
> And really, noreturn ought to be enough to infer that on its own.
noreturn only works for functions which never return. Most panicking functions do return, just not all the time.
Sorry, I meant that any function that was noreturn could be inferred to take up to forever to return (since it always takes forever to return, since it never returns). Then, any function that calls such a function (in a non-DCEed position) can be inferred to also take up to forever to return (even though it might take less than that), because some possible code path through it (it's up to X amount of time to return) takes forever due calling a function that takes forever.
* Rust doesn't infer function types (on purpose)
* like const (or Copy), the removal of the flag is a breaking change, so it should not happen implicitly
So functions would necessarily have to be explicitly marked as e.g. `convergent` or whatever. And while callers of `!`-functions would be easy to check for, I don't think rustc is currently smart enough to infer the totality loops in general.
> I don't think rustc is currently smart enough to infer the totality loops in general.
Well, yes; we're necessarily talking about changes to the language (or, equivalently, possible different languages).
> while callers of `!`-functions would be easy to check for, [other problems wouldn't]
That was my point, thanks: "when trying to save your crucial data to disk, in real-time code where panicking would maybe kill someone in the real world, etc", checking for the easy-to-check-for problems doesn't actually suffice to prevent disasters.
echo 1 > proc/sys/vm/panic_on_oom
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.
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.
It seems programmers want errors to handle themselves. It's like they want errors to automagically hook themselves into the entire code base and do "the right thing". Or maybe in other words: programmers mistake errors-as-additional-data, with errors-as-automatic-runtime-behaviour.
Am I crazy?
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.
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:
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.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.)
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
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.
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.
> 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.
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.
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()`.
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
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 Go has a second issue, which is that in 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: will work fine, with no complaint. Despite possibly passing complete nonsense to Bar if Foo is in error. Also works with Funnily enough Rust will also warn in both those cases, because it tries to track individual writes.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.
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.
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?
First of all, because a religious subculture will run to pitchforks and torches at the slight mention of the word exceptions in the room.
Then the overall machinery in place to make them work alongside code that was most likely written as C originally.
And naturally every ms and byte lost to exceptions infrastructure is a capital felony, so even more fine grained semantics to make it happen.
C++ is still the language I reach for when not doing any JVM/.NET language, but things like this is what takes the fun out of using it with another ones on a team.
I'm not sure what you mean here, neither article is about the low-level issues that arise in C++. The second article only discusses C#. Chen's point is more about the way every call can surprise you by causing your code to return early.
At least with exceptions you get a "I told you so" if something does happen, and a defined way to force application termination if the error isn't properly handled.
Given the remarks on the C++ post, I assume he belongs to the /EH- side of pitch forks and torches, which actually goes against the C++ best practices at Microsoft, https://docs.microsoft.com/en-us/cpp/cpp/errors-and-exceptio..., which leaves a feeling of a kind of biased article in some way.
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.
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.
The Java team adopted the ideas of CLU, Modula-3 and C++, yet it keeps getting the blame while being the fourth language implementing the idea.
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.
``` let x = Some(1); let x = match x { Some(x) => x, None => return, };
assert_eq!(x, 1); ```
I'm using this in my own code now to unwrap or return (it looks stupid):
[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
[0] https://doc.rust-lang.org/nightly/unstable-book/language-fea...
[1] https://github.com/rust-lang/rust/issues/31436
(in the context of walking a tree...):
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.
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.
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.
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.
But! To each their own!
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
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.
And as the executable gets larger and refactored, some of the "executable" code will inevitably become "library" code. What then?
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!
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.
Maybe that day will be a day after _f$#$@@#$_ _crashing_ stops being the easiest and most idiomatic way of handling unexpected conditions.
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.
In rust, my experience is that it's far more common to unwrap an empty result and just toss a panic which spams some almost completely inscrutable error at the user.
I'm not sure what the cause is-- it's been suggested to me by a heavy duty rust user that part of it is norms for library code (where exiting-as-error-handling would not be very welcome) and applied them to the whole ecosystem.
I've also been told that it's not uncommon for people to use library code for handling stuff that would be manually written in C but then the library code under-documents what the failure cases are and so the caller can't easily/obviously do anything more than panic on an empty result.
Whatever the cause the end result is that literally every single program I pull off github written in rust I encounter some panic. In fact, as of a about a month ago even firefox started regularly crashing for me w/ some panic in servo (triggered by twitter).
The majority of the panics I get from stuff are just trivial "You used the wrong command-line options", but it's hard to resist the impression that software written in rust is simply a lot lower quality. It might well be that some of this is software that wouldn't even have been published had it been attempted in C, or that it's just because a greater percentage is written by developers who are new to programming (or at least new to systems programming)... but that isn't observable.
The net result though is that it leaves me feeling uneasy and dubious about the claims of rust lowering defect rates, especially since there has been no formal validation of those beliefs. It sounds credible for sure, but if rust is freeing up programmers to worry about things other than memory safety, it's not showing from my experience. You can go back 5 years in my history and see that I promoted rust a lot -- I even opted for my former company to use it. But that was before encountering other people's rust code in practice.
I think for years java suffered greatly from similar bad impressions-- where you don't expect to be able to run something you found without dealing with a maze of inscrutable null pointer exceptions.
The library should "back off" by aborting anything it is doing and indicating failure without crashing or exiting the process (a return value indicating an error, setting the "errno" variable, making an error pointer provided by the caller point to an error object/struct, ...). Of course, the other side of the coin is that caller (the application or another library) has to actually check for that error indication, and determine what to do about it.
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.
[0]: https://docs.rs/eyre/0.6.5/eyre/
It’s Haskell’s do all over again and pure vs impure.
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.
I recently tweaked my application's failure reporting [3] to go from this embarrassing thing:
to the more useful: 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...
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.