Previous: Software Totemism
If you don't mind your program crashing once in a while — and I am not making fun here: for some programs making them 100% error proof doesn't pay off — using exceptions for error handling is pefectly adequate.
However, when writing code with strong reliability requirements your priorities are different. After all, large fraction of production outages comes from botched error handling. You really want to stop and think about each error code at every level as it is passed up the stack. In that case C (or Go) style error handling is exactly what you want.
The problem with C (and Go) though is that it allows you to ignore the errors. It's perfectly all right not to check the error code or even not capture it in a variable. For example, this kind of thing is a common, almost a standard way to close a file descriptor:
close(fd);
However, POSIX says:
The close() function may fail if:
[EIO] An I/O error occurred while reading from or writing to the file system.
Oops!
All that being said, I wonder whether there is an imperative language that enforces explicit handling of errors. For example like this:
x = foo(y) err {
// Handle the error here.
}
In fact, you would still be able to ignore the error but you would have to make a conscious decision to do so:
x = foo(y) err {}
Moreover, looking for ignored errors, which is really hard in C, given that there's no easy way to distinguish void function invocations from unchecked error codes, would become easy, both for human reviewers and for automated tools.
Martin Sústrik, December 19th, 2015
Previous: Software Totemism
We handle this by making an ignored return value into a compiler error, which is an option in clang, and gcc as well. Not exactly what you're looking for, and sometimes inconvenient, but in all I think it works pretty well.
There's also an attribute you can add to a function, warn_unused_result, which will fire a warning for code which calls it and ignores its return value. Useful if you control the API and want callers to pay attention to your return value. Still dependent on compiler support, but pretty useful.
Rust encapsulates all results of a function that might give an error behavior with the Result trait.
https://doc.rust-lang.org/std/result/enum.Result.html
People will have to explicitly decide to not check if there is error.
The problem is more general than this though. Given a pure function, we do not have tests that check for every possible input. If the compiler knew of the types of input that a function will be given, then it could require that tests be provided.
Yes, both of the approaches above achieve the goal.
What I had in mind though — although I haven't expressed it explicitly — was taking the good parts of exception handling and cut off the bad parts. So, on one hand, you'd have clear decoupling of core logic and error handling (no errors as output params) while on the other hand there would be no magic "goto from" behaviour:
(raise, err and errorcode being keywords)
The example is rough at the edges, but you get the idea.
So, essentially you want checked exceptions that don't propagate upwards on the stack and must be handled by the caller.
It's interesting - this is very close to what one of the proposals for Rust language contains:
https://github.com/rust-lang/rfcs/pull/243
Namely, it proposes to implement something akin to exceptions (with easy "throwing" and "catching" them) over the regular Result. Essentially this is just a syntactic sugar, but in my opinion it is indeed a very convenient approach - there is no stack unwinding and implicitness of exceptions (since these are just regular returns and special return type in functions, and error values must be propagated explicitly), but syntactic cleanliness of exception handling (when it is needed) is still there.
The Rust language has the match command that matches patterns (similar to haskell).
Match is then used on Result and the 2 cases (Err,Ok) are handled there.
The error handling happens close to the point that the error happened.
I think you will like Rust.
Hi Martin!
I find your opinion about Rust and it's error handling very interesting. I am a little bit confused because all great libraries and systems are written on C, but now Rust is widely promoted for these issues. Would you comment upon this please? And are you going to use Rust?
Rust and Haskell do it with a specific type, but you could argue that Java does something like this too. Checked exceptions basically achieve the same thing.
What do you think of *checked* exceptions?
See http://norswap.com/checked-exceptions/
https://github.com/kisielk/errcheck makes this super easy to find in Go. I do find that my programs end up running better with the error approach of Go than the exception based approach of Java.
Perl has eval {}
I know checked exceptions have a bad reputations, but they are forcing you to handle them.
Some studies comparing C to Java code show that the Java code on average has better error handling.
I wrote quite a lot about error handling here:
http://codemonkeyism.com/some-thoughts-on-error-handling/
Post preview:
Close preview