5 ms·
My kingdom for someone who can figure out how to solve error handling. My code consists of some reasonably straightforward sequence of actions with a random sm
by rogerbinns 14y ago
My kingdom for someone who can figure out how to solve error handling. My code consists of some reasonably straightforward sequence of actions with a random smattering of error handling significantly distracting from that.
That error handling code is tedious to write, very time consuming to test (and often virtually impossible) and usually not run very often. Exceptions at least let you put the handling code somewhere other than the normal sequence (although you may still have some finallys), continuations are quite nice, and the Go/C model pollutes the code but puts the error handling right next to the error detection.
None of these really solve the problem though. How can I have the least amount of error handling code possible, how can I test it, and how can I be sure it is correct, and all while spending my mental efforts on the code that actually does useful things?
- kevingadd 14y agoThe Erlang process model seems like the closest thing to a panacea here. It forces you to write robust software automatically. I haven't used that approach enough to know if it gets you the whole way to shipped, though. (Note: Not specifically advocating Erlang The Language, just its approach to isolation and error recovery)
- pcwalton 14y agoRust uses the Erlang model. Generally failing the task on error has worked well in practice, although there are times when we have to use result types and pattern matching (basically error codes, but more reliable since the compiler forces you to use them correctly). The vast majority of errors are handled either locally or by failing the task.
- rdtsc 14y ago> Note: Not specifically advocating Erlang The Language, Why not. If you have a panacea for robust error handling (they call it fault tolerance) coupled with auto-magic cross-CPU and (with some work) cross-machine scaling why not advocate the language. Anyone else will just be re-implementing that in an incomplete way. So you can have say an OS process like an Erlang process. That's one way. But now need to implement a supervision hierarchy. Ok so talking about SIGCHILD, pids and what signal to use to kill the process (SIGTERM or SIGKILL). Then of course these processes have to talk to each other so must pick a distributed messaging system. Then need to pick a restart strategy (ok so the process failed but we restarted it but it keeps failing and we keep restarting so perhaps that in an of itself is a failure and a higher process in the chain should be restarted...). Stuff like that. By the time that's done and looking back at the code you'd wonder, why didn't I just used Erlang with all this built it...
- yanatan16 14y agoYou don't want a language to force error handling on you. Error handling is different in every case. Go only tries to force you to deal with errors instead of allowing you to ignore them until an exception is thrown. There is never going to be an all-purpose error handling answer. Its just a matter of deciding where your error handling code is placed.
- eckyptang 14y agoThis is the most sensible comment I've ever seen on error handling and one I agree with entirely. When designing the architecture for an application, error handling is actually the first thing I look at. It's different depending on where it is, who needs to handle it and the context it applies in (technical or business domain). There is no "one size fits all" solution.
- Chris_Newton 14y agoMy kingdom for someone who can figure out how to solve error handling. You don’t really seem to want someone who can solve error handling. You seem to want someone who can make it disappear, or at least get as close as possible to that ideal. I feel your pain, but I fear you are asking the impossible. :-( Some errors are recoverable, and maybe it would be helpful to have default policies like “if the file is read only, have the OS prompt the user to make it writeable and retry” to take care of a lot of the repetitive coding work in those cases. At least then we wouldn’t have to do things like writing a top-level file-handling operation in a loop that tries (in the exception handling sense) to do something, and if any recoverable error occurs, attempts a suitable recovery and goes back to try again. On the other hand, to have some sort of default policy, you’d need something in the semantics of your language to indicate which policies should apply and where. By that point, you might not be doing much better than a try-catch in a loop. In the end, though, not all errors are recoverable, and that is the fundamental problem. Some errors are fatal, and you just can't carry on as normal or rely on some automatically generated default behaviour, because the semantics of your program are now broken. What would you want to see that you don’t have today in terms of minimising this kind of error handling code or making it easier to test? I don’t know what kind of testing you have in mind, but one thing I’d really like to see going mainstream is a serious effect system built into the type system of the language. That would be useful for a lot of reasons, one of which might be checking that whatever error handling constructs were available didn’t do things like leaving effects half-applied, transactions open, or resources locked, in the event of a change of plan because of an error condition. I expect some relatively near-future languages will start to pay more attention to formal effect systems as fields like distributed systems, concurrency and security become ever more important in the programming mainstream. But this is a topic far bigger than just error handling...
- rogerbinns 14y agoI agree that zero lines of error handling is the perfect solution. Even getting error handling and testing to be a trivial percent of the actual useful code would be a major improvement. At the moment my testing code is around the same size as the useful code plus error handling, and error handling is around 10% to 50% of the code. I think part of the problem is error reporting is still somewhat shaky. To a certain extent you only actually need to handle errors that occur in the real world. Some languages like Java and Python can get you a reasonable amount of information when an exception occurs, but you still have to find mechanisms to report it back, and chances are you won't have detailed program state or operations leading up to the error. But if you did you could in theory release programs with no error handling as alpha releases (in the original sense, not current marketing usage) and see what shakes out. > I don’t know what kind of testing you have in mind Using fdr's example of a cat program, it consists of an open, a sequence of read followed by write and a close. I would like to point the language/environment at that and say "test it" with no further effort. Currently I have to write far more code to test, and then add in instrumentation to force various failures (eg make close return an error). The test code in this example would take considerably longer to develop and could itself have bugs!
- fdr 14y agoYou and I sound like we may be cut from similar cloth. I find Go's approach to error handling -- which is to not to do much about the problem at all -- is better than pervasive use of exceptions but worse than some other possible mechanisms (and on this we may disagree). Although I'm not saying the language Rust has got it right, perhaps some of the thoughts on the mailing list may be useful. However scarily, my mailing list post -- which only asks how to handle errors -- still ranks on top for "rust error handling" on Google: https://mail.mozilla.org/pipermail/rust-dev/2012-March/001454.html https://mail.mozilla.org/pipermail/rust-dev/2012-March/00145... Here I mention the Common Lisp condition system, which has been my favorite so far, but the one of the principal authors of Rust mention a somewhat similar but lighter weight alternative.
- pcwalton 14y agoWe're interested in adding support for condition handlers. Arbitrary stack unwinding and/or first-class continuations aren't likely due to runtime/toolchain limitations, but the "register a function to be called on error, which might direct the call to be restarted" style is intended to be supported.
- rogerbinns 14y agoI actually prefer exceptions to Go's approach, mainly because most of my code is structured with the best place to handle errors being higher up the call chain. Exceptions let me do that for free, whereas Go requires boilerplate code all over the call chain. BTW your posting left out that close() can also fail (eg deferred writes run out of disk space). And note that none of these really help with testing. In your 'cat' example, I'd want to test open failing, read failing, write failing and close failing. At a certain level the combination of language and libraries know these can fail. Ideally I would like to write zero lines of code to exercise those cases. In practise I usually find out that my test code is larger than the code being tested, and I usually have to add all sorts of instrumentation to force various failure modes which again makes my original code even longer and more obfuscated.
- PuerkitoBio 14y agoGo's panic/recover pattern lets you handle exceptions in the place you feel is best, much like exceptions. Though it is true that you would still need to check the error returned, and panic if not nil. However, there are a few libraries that offer 2 versions of the same functions, one that returns the value and error combo, and one that panics in case of error and returns only the value (regexp' Compile and MustCompile for example). When it makes sense, it's nice to have this option. Anyway, error handling is hard, I'm not a fan of exceptions, though alternatives such as Node's error as first argument of a callback and Go's second return value require polluting the normal code path. I like go's "defer" idea as a "finally", though.
- lkrubner 14y agoLisp had a condition system that was very clever. You still had to write the code, but you were able to separate the problem, the handling, and the restart. This is the problem that comes up in many languages and which needs to be dealt with: "Because each function is a black box, function boundaries are an excellent place to deal with errors. Each function--low, for example--has a job to do. Its direct caller--medium in this case--is counting on it to do its job. However, an error that prevents it from doing its job puts all its callers at risk: medium called low because it needs the work done that low does; if that work doesn't get done, medium is in trouble. But this means that medium's caller, high, is also in trouble--and so on up the call stack to the very top of the program. On the other hand, because each function is a black box, if any of the functions in the call stack can somehow do their job despite underlying errors, then none of the functions above it needs to know there was a problem--all those functions care about is that the function they called somehow did the work expected of it. In most languages, errors are handled by returning from a failing function and giving the caller the choice of either recovering or failing itself. Some languages use the normal function return mechanism, while languages with exceptions return control by throwing or raising an exception. Exceptions are a vast improvement over using normal function returns, but both schemes suffer from a common flaw: while searching for a function that can recover, the stack unwinds, which means code that might recover has to do so without the context of what the lower-level code was trying to do when the error actually occurred. Consider the hypothetical call chain of high, medium, low. If low fails and medium can't recover, the ball is in high's court. For high to handle the error, it must either do its job without any help from medium or somehow change things so calling medium will work and call it again. The first option is theoretically clean but implies a lot of extra code--a whole extra implementation of whatever it was medium was supposed to do. And the further the stack unwinds, the more work that needs to be redone. The second option--patching things up and retrying--is tricky; for high to be able to change the state of the world so a second call into medium won't end up causing an error in low, it'd need an unseemly knowledge of the inner workings of both medium and low, contrary to the notion that each function is a black box. Common Lisp's error handling system gives you a way out of this conundrum by letting you separate the code that actually recovers from an error from the code that decides how to recover. Thus, you can put recovery code in low-level functions without committing to actually using any particular recovery strategy, leaving that decision to code in high-level functions. To get a sense of how this works, let's suppose you're writing an application that reads some sort of textual log file, such as a Web server's log. Somewhere in your application you'll have a function to parse the individual log entries. Let's assume you'll write a function, parse-log-entry, that will be passed a string containing the text of a single log entry and that is supposed to return a log-entry object representing the entry. This function will be called from a function, parse-log-file, that reads a complete log file and returns a list of objects representing all the entries in the file. To keep things simple, the parse-log-entry function will not be required to parse incorrectly formatted entries. It will, however, be able to detect when its input is malformed. But what should it do when it detects bad input? In C you'd return a special value to indicate there was a problem. In Java or Python you'd throw or raise an exception. In Common Lisp, you signal a condition. A condition is an object whose class indicates the general nature of the condition and whose instance data carries information about the details of the particular circumstances that lead to the condition being signaled.3 In this hypothetical log analysis program, you might define a condition class, malformed-log-entry-error, that parse-log-entry will signal if it's given data it can't parse." http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html http://www.gigamonkeys.com/book/beyond-exception-handling-co... Read the whole thing. It was a very clever system and wish something like it was available in other languages. (The lack of it in Clojure was one of the few things I agreed with Loper about in his anti-Clojure rant: http://www.loper-os.org/?p=42 http://www.loper-os.org/?p=42)
- MetaCosm 14y agoErlang has a strong philosophy on this -- let your process (a microprocess inside Erlang, not the entire VM obviously) crash rather than pollute your code with needless endless crap... dozens of try catches just do log that an error happened... that is insane. Death to defensive programming which is a massive endless blackhole to throw developer resources down! Due to the supervisor / worker model of Erlang -- if you don't know how to explicitly handle an error -- YOU DON'T! You simply let the process crash! You be tight with your pattern matching (you can think of them like assertions) and you let Erlang do its thing. {expected_thing, 55, SomeVarToCapture} = function_call(..) If the function doesn't return something matching {expected, 55, ...} it blows up -- it crashes... and this is fine. Because in most cases you don't know how to fix that problem anyway! But, it doesn't have to Erlang has try/catch when you want it -- for those cases when you CAN handle an error, you do know what to do to fix it... which is the point -- when you can HANDLE the error, you do... else let it go SPLAT.
- jacques_chester 14y agoException handling exists in the problem domain, not just the solution domain.
- tikhonj 14y agoI've always liked the Haskell approach of using a monad like Maybe or Either. Maybe is a special case where rather than having an error you have a null--perfect for functions like indexOf--but since it is isomorphic to Either (), all my points apply to both. The most important point is that in Haskell these errors are reified as first-class types. So the type `Either err val` is just like Go's method of returning either an error code or the correct result. However, rather than being a special case, this is just a normal algebraic data type. This is a more elegant way to support this sort of error-handling, as opposed to just baking it into the language explicitly. Being first-class citizens is nice, and we'll get back to it, but first let's look at another way they're better than Go's approach--they form a monad. There is some rich theory behind this, but it also has an immediate practical benefit: you get the default action of propagating the error for free. That is, code like this: if isError riskyValue then error else if isError (riskyFunction riskyValue) then error else ... transforms into: do value <- riskyValue result <- riskyFunction value ... so you get the simplicity of Go's approach with the convenient propagation semantics of normal exceptions. Now you only have to check for an error when you want to handle it; it gets sent through transparently if you don't. This is also cool because you can use the Either type to model early termination rather than an error, which is why it's called Either rather than Error. So, being a monad, the Either type is nicer to use than normal returned error codes. But earlier I mentioned that there are some advantages to being a first-class citizen. What are these? Well, the main advantage is that there is a fair number of generic functions that can be used to make your error-handling code neater. For example, you can use the alternation operator in a pattern I really like: canError1 a b <|> canError2 a b <|> return 42 what this does is try the various options one by one until it either finds one that isn't an error or gets to the end of the expression. The last element could be a default result, as here, or a default error. Another fun combinator is optional: do someImportantAction 1 2 val <- someValueOf 10 optional (optimize something) return something Another really cool thing is how types like this can interact with other types. In particular, I am thinking of monad transformers. In simple terms, monad transformers allow you to combine different "effects"; for example, you could combine error-handling as here with backtracking. But here you have two options: an error can either make the entire backtracking computation fail or it can just make a single branch fail. Which one should you choose? The cool answer is that it is, indeed, the programmers choice. In particular, the order of transformers controls these semantics; something like EitherT err (Logic a) would be the first and LogicT (Either err a) would be the second. Not only do the types reflect the semantics, they actually control them! Very much self-documenting code. The Haskell approach gives you high-level, declarative and very extensive control over exactly how you want to handle errors while hiding enough of the normal boilerplate to make them convenient to use. All this in a way that is not baked into the language but just an instance of a more general pattern (in this case a monad and monad transformer).
- zaptheimpaler 14y agoGreat idea, I completely agree. Error-handling code is pretty much impossible to eliminate since often the compiler can't possibly know what you want to do when theres a certain error. I think the solution is to use a RoR kind of philosophy, "convention over configuration". Not exactly that, but basically sane defaults. Maybe this would get better if/when compilers get extremely intelligent.
- vrotaru 14y agoI remember reading a paper proposing error handling to be done only at function not block boundaries. A minimal change from the normal exceptions will be something like this: function foo(bar, quux, zut) try { var a = frobincate(bar, quux); return bubinate(a, zut); } catch (e) { // Stuff } finally { // Stuff } Whether such a change will make code easier to write, read and maintain is hard to say. Writing a language that compiles to JavaScript and uses that syntax shouldn't be that hard. I'm partly tempted, but I almost never program in JS
- viraptor 14y agoI don't think it would be that nice. For good error reports back to the user, you'd have to have a fairly large amount of logic in the catch block. If you have a function that copies data from one file to the other and catch some sort of IOException, you'd end up trying to analyse what it is to report it properly. After all, it could be either one of the files, while opening, closing, reading, writing, ...
- vrotaru 14y ago_For good error reports back to the user, you'd have to have a fairly large amount of logic in the catch block._ Serious question. How is that a reason to have that _amount of logic_ in the function body? Having IDE/editors which can fold blocks of code and having the catch blocks collapsed by default will make code reading/understanding of code easier. IMO. Actually, I may try something like makng the try-cacth the body of the function on a small to medium size project just to see the tradeoffs.
- viraptor 14y agoI think that putting it in the body actually reduces the amount of code and makes it more readable. Compare something like this (ignoring naming, formatting, made up language, etc. I want to show the structure only): fin=open(fin_name) fout=open(fout_name) ... catch IOError e if e.call == 'open' if e.filename == fin_name log_error "cannot open input ..." elif e.filename == fout_name log_error "cannot open output ..." else log_error "cannot open file, but filename unknown" elif e.call == ..... ..... to something like this: try fin = open(fin_name) catch IOError log_error "can't open input ...." return try fout = open(fout_name) catch IOError fin.close() log_error "can't open output ...." return Both are ugly, but I really wouldn't like to be the person trying to match up the error handling from the first example to the exact places in the function body. I'm not even sure what would this look like if the exception was coming from somewhere deeper in the function. open() is easy because we know how it fails. More complex call chains would be a challenge to even match up to the place they happened.
- DeepDuh 14y agoI've found that the least amount of mental overhead is still achieved by doing all the necessary tests right where the 'useful' code is taking place, raising a standard Exception but with a good error description and doing a Pokemon-catch somewhere in main(). Not good enough for system programming of course, but for everything you want to fail early it's fine. If we do talk about system programming: Error handling IMO is its main difference to 'other' programming. The necessity of deciding (and in case of security relevant systems testing) for all eventualities is why only veteran programmers should do that job.