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/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.
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.
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.
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.
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...
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!
OK, so I suppose the next question becomes what “test it” means.
Perhaps you want your test system to make some kinds of automatic deductions about the intended behaviour of your code and then to verify that the actual behaviour matches? If that is the case, what kinds of deductions would you want to be handled automatically for you?
Or maybe you’re thinking of some sort of automatic simulation of possible failure cases when you get to the open/read/write/close operations, serving a similar roles to things like mocks and stubs in unit testing?
Some of these are the kinds of problems I’m hoping a good effect system will help with. If you know that the operations of opening, reading, writing and closing files must occur only in certain orders, you can automatically detect violations of those rules. And if you know all possible types of cause and effect that can be relevant within each part of your code — that is, you can identify all stateful/impure/externally observable results — then you can identify any of them that aren’t handled according to some set of rules. As long as you can figure out what you want those rules to be, that is...
It would be something similar to all possible code paths being executed with the program in all possible states. Heck right now I'd settle for even a small subset of this.
> Or maybe you’re thinking of some sort of automatic simulation of possible failure cases when you get to the open/read/write/close operations, serving a similar roles to things like mocks and stubs in unit testing?
That is closest to it. It is already known that open/read/write/close can fail. They are quite difficult to force to fail, so currently you have to write a lot more code to make that happen. And it gets really tedious once you look at all the combinations. eg open has to fail, open has to succeed and then read fails, open succeeds and the 3rd read fails and on and on. This is for a single ~5 line function! And then if my cat() is made available to a library, how to they test for it failing.
I'm quite happy using supervisory programs to detect rule violations. For example valgrind does an excellent job for memory allocation and usage, and I've never used helgrind but assume it works well too.
OK, I’m following you so far.
Assuming that
(a) our language lets us specify the possible failure modes for each function, and
(b) we have a test tool that can systematically simulate various possible combinations of success and failure,
what would you want to do in each test case?
Put another way, when we run our test tool and our magic replacement I/O functions simulate, say, success on the open and first two reads but then failing on the third read, what happens next? What’s the result we’re looking for to determine whether the test passes or fails?
My test code currently has to put the program into a state where cat can be called, and then has to mock/augment each failure point, and then check the results. Just having the middle piece (mock/augment) done automagically would be a massive help. And then of course cat itself can fail so each of its callers needs a way of testing too. Some hand wavy combination of convention over configuration, documentation and annotations would likely help.
Even the state issue should be automatable in many circumstances. For example you could make a successful run of the program, and then the magic records state at the entrance to code blocks. It can cause each failure circumstance, rewind state to known good and cause the next failure circumstance etc.
I'd even be happy running my code (with no error handling) under some tool that induces the errors - when they aren't handled it asks me what I want to do which typically involves writing code to handle the issue, which it then directly integrates and keeps running until the next error.
Even test pass/fail can be somewhat automatable. The tool records what happens, and then in the future alerts you when there is a difference in behaviour. The response is either that the issue needs to be fixed, or that the new behaviour is correct.
(Note: Not specifically advocating Erlang The Language, just its approach to isolation and error recovery)
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...
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.
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-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)
def parse_log_entry(text, err_fn):
entry = parse(text)
if entry is None:
entry = err_fn(text)
return entry
Instead of passing a function, err_fn could also be some kind of resolver that finds a function for a condition name, or there could be a global recovery function: def parse_log_entry(text):
entry = parse(text)
if entry is None:
entry = recover('invalid_log_entry', text)
return entry<Devil’s advocate>
But if there was some way for medium to recover by itself, it had the option to handle the exception on the way back up the call stack and then to restart the computation that called low, hopefully under improved conditions.
Control will therefore return to high only if medium doesn’t itself know how to handle the error. At that point, the entire computation for which medium was responsible has failed.
Assuming the system as a whole uses reasonable functional decomposition and modular design, since exceptions have limited value under other circumstances anyway, shouldn’t any context that was known only within medium be irrelevant to any recovery action taken at high’s level?
</Devil’s advocate>
This seems like a large step in the right direction for exception handling, but I think it still has the problems that the programmer writing the function that can throw needs to enumerate a number of cases to make it effective, and the programmer calling that function needs to have documentation ready for a descriptions of all the possible restarts.
NB: Google will then have all your base, and people on HN have recommended turning off google web history altogether (let alone the toolbar!). I mention it, because it is also a killer-solution to the common problem you mention.
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 JSSerious 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.
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.def hello return do_someting rescue FooError return :fail end
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).
EitherT err Logic a
LogicT (Either err) a
(The monad transformer takes the monad and the value type, not the monad applied to the value type)Maybe gives you binary error handling, it either fails (Nothing) or succeeds (Just).
Either gives you "stringly-typed" errors which is sometimes a good choice.
http://hackage.haskell.org/packages/archive/mtl/1.1.0.2/doc/...
Either doesn't have to be "stringly typed" per se thanks to sum types. You can write a type like:
data ErrorType = SomeError
| OtherError String
| ...
The compiler can now check that you handle all possible errors and will give you a warning if you don't, so it's strictly better than using strings.Coincidentally, this is one place where some sort of sub-typing would be nice, I think. This sort of type would be perfect using OCaml's polymorphic variants because then you could share different error cases around while specifying a very specific type for each computation.
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.
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?