13 ms·
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
by kstenerud 6y ago
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.
- Ygg2 6y ago> 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.
- mhh__ 6y agoThe only system I think should completely go is Exceptions except in the case of termination or absolutely catastrophic failure - this isn't really about programming but rather that the implementation is a total pain, the compiler struggles to optimize them, and even better they make quite a few safety analyses like borrow checking very difficult because the control flow graph basically explodes when you start considering exceptional control flow - even on a basic block
- simiones 6y agoIn terms of control flow, this: try { someCall(); } catch (ErrorICanHandle err) { //handle } perfectly equivalent to this: err = someCall(); if canHandle(err) { //do something } else { return err } I don't see why so many people think exceptions make code harder to analyze. In my opinion, it is errors themselves that make code hard to analyze, regardless of implementation strategy. The only difference between exceptions and error returns/error codes is in human readability: one makes the bubbling behavior explicit, which clutters the code but makes it obvious; the other makes the bubbling behavior implicit, which keeps the code cleaner, but also less obvious. After writing professionally in both languages with Exceptions (Java/C#/Python) and in Go, I much prefer Exceptions to error values/codes, but I can appreciate that it may be different in other domains or for other people.
- xpressvideoz 6y agoI mean, Go's error handling is one of the worst implementations of the "error code" idea, so it is a bit unfair to disregard the idea just because one particular implementation is bad. You should try Rust or Swift, at least the error handling part, to fully appreciate what a good error code implementation could be.
- simiones 6y agoI'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.
- sensitive-ears 6y agoThe "?" 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)
- chmod775 6y ago> Error handling has been wrong since the beginning, and has continued to be wrong ever since. Maybe it's not error-handling that is the problem then, but undisciplined software developers writing bad code? Out of all the engineering disciplines, us software engineers have to be the worst bunch by far.
- nx7487 6y agoYeah, my question is, what's the best direction to go in: 1. Continue trying to create error-handling systems for devs 2. Try to solve this with more generic static (or dynamic) analysis tools. I mean, maybe what we need to do is make simulator-esque testing more popular, or something.
- heavenlyblue 6y ago> Attempting to get the complete set of error types that any given call may raise is a fool's errand because of the halting problem it eventually invokes. That’s not true.
- im3w1l 6y agoYeah, it only becomes an issue if you want the compiler to infer that ImpossibleError cannot be raised in while True: pass raise ImpossibleError But normally we are perfectly content to put ImpossibleError in the signature for code like this.
- heavenlyblue 6y agoActually in this specific case you can infer that, and you can have an exhaustive list of language constructs beyond which this becomes undecidable. There is a practical example of “panic!” in Rust to go around that as well.
- why_Mr_Anderson 6y agoBecause all systems try to handle several kinds of unexpected behavior in the exactly the same way - invalid arguments (e.g. 'x == null') - code logic (e.g. 'if (x.salary < 0) ...') - external errors (e.g. 'out of memory') I'd argue that only the 3rd kind is actually 'exception', it's completely out of program's control. Code contracts are wonderful way of dealing with 1st and 2nd kind, sadly they didn't catch up and remains mostly unknown. They are vastly superior to tests and also serve as much better way to document intent of the code, how the author expected the code to work.
- nerdponx 6y agoUnless you have dependent types, isn't a contract just syntactic sugar for an if/else that either raises and exception or returns an error code?
- enriquto 6y ago> 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.
- hacker_9 6y agoThe issue is we have to pre bake decisions into the application, and until the application is literally an AI this is as good as it gets. When building a program 'errors' occur all the time, but we make design decisions to handle them because we can identify the right path forward in the given context.
- Kaze404 6y agoIsn'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).
- creata 6y agoYep, although I guess the Try trait[0] and the corresponding ? operator count as a special error handling language construct. [0]: https://doc.rust-lang.org/std/ops/trait.Try.html https://doc.rust-lang.org/std/ops/trait.Try.html
- ymbeld 6y agoThe 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.
- mrmr1993 6y agoI 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.
- simiones 6y agoThe biggest problem with that is that it is very unwieldly if you're using any kind of higher-order functions. To fix that, you need to start supporting error polymorphism. For example, `map` should have a signature like map :: List a -> (a -> b -[err]) -> List b -[err] So that map [1 2 3] +1 //no errors map [1 2 3] sendOnNetwork //returns NetworkError At least, this is one of the major limitation of Java's Checked Exceptions idea, one which often forces you to use Unchecked Exceptions. Another common problem is handling exceptions which occur while handling another exception. D has the best default I've seen in that area (it will automatically collect all of the errors, instead of panic()-ing like C++ or discarding the old exception like Java and C#).
- junke 6y agoAnd if the function can throw different kinds of exceptions, you need a way to express the union of unrelated exceptions (without defining a new variant type), a bit like Polymorphic Variants I guess.
- dllthomas 6y agoIdeally 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.
- jiehong 6y agoWould you put the Erlang/Elixir error handling in the "catch Throwable" camp? Because in this case, the error handling is done in a different process altogether (supervisor).
- Floegipoky 6y agoI 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.
- infensus 6y ago> 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.
- dilap 6y agoAre you familiar with Zig's error handling? Except for the fact that errors cannot contain payloads, it is, imo, perfect.
- jiehong 6y agoit has the same issues. You can always discard errors with `catch unreachable` for example.
- malcolmstill 6y agoOf 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.
- higerordermap 6y agoUsing exceptions for control flow vs using control flow for exceptions. Who will win?
- brundolf 6y agoIt's not perfect, but I think Rust's approach is the best one yet. > Any error system that handles all errors the same way will fail because there are some errors we can ignore, and some errors we must not ignore. Rust has separate categories for these two things. Panics cannot be handled, which pushes the author to use them sparingly. Results must be handled (or explicitly elevated to panics, in a way that's easy to track down later). The main weakness of this system, imo, is that the thrower, not the caller, decides whether or not a given error must be handled, and sometimes the answer is "it depends on what the consumer is doing". That said, the powerful (mainly, no-implicit-null...) type systems that are starting to become prevalent prevent a lot of cases that would have been errors 20 years ago from existing in the first place. In modern typed languages an error is almost always a genuine IO/environment anomaly, which means there are a lot fewer of them to handle in the first place. Edit: The other big weakness of Rust's system, as mentioned in the article, is the jumble of all the different disjoint Error types. I think the core issue here is that Rust doesn't have ad-hoc union types. I.e. if you want to say "this value is of type X or Y or Z", you have to declare a new enum somewhere, and embed your possible types in each branch, and that's a lot of ceremony at each point where you bubble up a different possible type of error. I understand why Rust's enums are static, though, I wonder if there's a place for some sort of heap-allocated ("dyn?") solution that allows any combination of possible types. Edit 2: Actually this probably wouldn't be possible because there would be no way to distinguish the union, because Rust carries no type info at runtime. Maybe there could be syntax sugar for anonymous enum types declared inline?
- mlindner 6y agoOne of the biggest issue with Rust’s panics is there’s many times when you must never panic. For example in an OS, when trying to save your crucial data to disk, in real-time code where panicking would maybe kill someone in the real world, etc
- kibwen 6y agoI 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.
- tasubotadas 6y agoNice summary. Ideally, I would like languages just to do simple exceptions (no checked stuff) and create a way to catch them.
- brazzy 6y agoThe 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.
- kibwen 6y ago> 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.
- ymbeld 6y agoI don’t get why this is off by default for debug builds though. Maybe that’s a separate concern.
- alpaca128 6y agoIn 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.
- 6y ago
- randyrand 6y agoi’ve use Java C++ and Objective-C and by far the best approach to errors has been Objective-C’s NSError* out parameters. They're strings. They’re chain-able. They’re visible. They’re hard to ignore. They don’t propagate or crash unless you want them to.
- dnautics 6y agozig 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.
- tus89 6y ago> Any error system that relies upon developer discipline will fail because errors will be missed. Haven't there been some languages that force functions to return some kind of tuple like: result,error And forces the programmer to at least do: if(error) { } It does not force any kind of correct handling, but simply oversights should be caught. I might be imagining things though.
- simias 6y agoThat'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()`.
- TacticalCoder 6y agoGP 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").
- masklinn 6y agoWhat you're describing is Go, except its requirements are much weaker than that. Rust, meanwhile, is way stricter than that, and has gone way further on the "not relying on developer discipline" path: a failing Rust function will return a `Result<Value, Error>`. You can't even access the value without explicitly checking whether it's a value or an error one way or the other, which you can very much do so in e.g. Go. Rust also has an attribute called `must_use`, which can be set on types and functions. That attribute causes the compiler to emit a warning if the marked object is not used at all (either the type or the function's result), so while go will not say anything if you write Foo() and that returns an error (or an error and a result you happened not to care for), Rust will absolutely complain by default in the same case, you will need to write at the very least let _ = Foo(); Go has a second issue, which is that in result, error := Foo() that you "have to" handle the error is a consequence of the unused variable check (can't define a variable and never read from it). However because it's that instead of something dedicated, this: id, error := Foo() result, error := Bar(id) if error != nil {} will work fine, with no complaint. Despite possibly passing complete nonsense to Bar if Foo is in error. Also works with id, error := Foo() if error != nil {} result, error := Bar(id) Funnily enough Rust will also warn in both those cases, because it tries to track individual writes.
- samatman 6y ago> 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.
- MaxBarraclough 6y ago> Then we had exceptions, which solved the problem of people forgetting to check by crashing the app. They can also harm readability, as just about every statement can now cause an early return from a block. They also greatly complicate static analysis. No discussion of the shortcomings of conventional exceptions would be complete without these two excellent blog posts by the great Raymond Chen: Cleaner, more elegant, and harder to recognize https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36693 https://devblogs.microsoft.com/oldnewthing/20050114-00/?p=36... Cleaner, more elegant, and wrong https://devblogs.microsoft.com/oldnewthing/20040422-00/?p=39683 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?
- pjmlp 6y agoThe 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.
- MaxBarraclough 6y ago> 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.
- 6y ago
- dataflow 6y agoI feel like error handling is fine in like C#. No checked exceptions, and stack traces come with the exceptions, and you can nest them. What's not to like?
- kelnos 6y agoIMO exceptions are a terrible way to signal errors. They obscure control flow and make it much harder to reason about a program's behavior. They make it easy to forget to check for errors, and encourage sloppy catch-all error handling. It is a lot harder to determine if exception-based code is correct or not, and languages that have exceptions require you to examine literally every line of code to determine if it can throw. Even the name is wrong: an "exception" should signal truly exceptional behavior, things that you would not expect to happen unless something is really wrong. And yet exceptions are thrown for mundane things like "file not found", something you could very easily expect to happen routinely. Exceptions should be like Rust's `panic!()`: only for things that the programmer can't reasonably do anything about, which will cause the program to terminate.
- dataflow 6y agoI 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.
- pjmlp 6y ago> Then the Java team got the bright idea to have checked exceptions 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.