Comment by enriquto
5 years ago
The very concept of "error handling" is absurd.
There are no errors, just unnecessary abstractions and control flow hacks. You try to open a file; either you can or you cannot, and both possibilities are equally likely and must be handled by normal control flow in your program. Forcing an artificial asymmetry in the treatment of both cases (as championed by the error handling people) adds ugly complexity to any language that tries to do so.
The problem of error handling arises due to type systems and function call semantics.
Most programmers are going to expect 'data', the result returned by 'open', to be a type that allows reading/writing the file. The programmer expects /var/foo to exist, or they might have checked before calling 'open', but even that's not foolproof.
Historically, a failure might just set 'data' to an invalid value (like 0 or null) but that ended up being a bad idea. And we needed some way to return more information about the error. So we started doing this:
But this mainly just complicated things. Is 'data' input or output? The function doesn't return its actual output. And it's still possible to ignore 'error', so 'data' is still potentially undefined.
Then exceptions were invented so we could use proper function call styles again, and the program wouldn't go into an undefined state. Instead, the error could be handled with separate logic, or the program would halt if it was ignored. This was far from a perfect solution, though.
Then sum types entered the mainstream, so 'data' had well-defined ways of returning something other than the expected result. But that resulted in a lot of competing conventions and stylistic decisions for what to do when 'data' is an error type, that haven't quite been settled yet.
Again referring to Go's imperfect but interesting handling of this problem, the style in Go would be:
In fact, the compiler will make you deal with both values after assignment unless you explicitly ignore one of the return values.
This way a combinatorial explosion lies.
Each library/system call you do can result in a set of possible consequences. We usually don't care about them equally, though: in fact, in the file example, 99% of the time we care about whether the file was opened or not - and in the latter case, we don't need to know why. So the asymmetry is already introduced by the intent of the program - there's usually only one path of execution we want; other ones are distractions. Error handling exists to express that asymmetry of caring at tool level.
I completely disagree. A program or function is designed to perform an operation. If that operation requires the contents of the file, then the program cannot continue unless it successfully reads the contents of the file. There is already a natural asymmetry. If you cannot open the file, there isn't any more to do.
An "operation" is not something inherent to the code. If we look at a function that may get what we call an error, we'll see that in either case it completes and returns control to the caller. We label one such result 'success' and another 'failure' because we also have an idea of purpose of the function, but the purpose does not exist at the code level. Maybe this is why we struggle with errors.
Once you give a function a name, it has an operation that's inherent to it.
If you have a function called `Add` that takes 2 parameters and returns the sum of those two numbers. That's the operation. If the web service that you call to perform the sum is down, it cannot return that sum, so that's an failure.
The code that calls this function needs that sum to continue it's operation. If it cannot get that result it cannot produce it's own result and that error needs to be propagated. Maybe the entire program has a purpose that it cannot now be completed and should be aborted.
2 replies →
That's a neat observation. If you're writing a function as "How do I get from A to B?", that's more error-prone than "What are all the possible outcomes of trying to get from A to B?"
I disagree; programming languages and code are built explicitly and exclusively to perform a function. Operations and purpose are more inherent to the code than their mathematical/logical nature.
1 reply →
> I completely disagree.
Disagreeing is alright, but here you don't really do, do you? I can translate the paragraph you have written into pseudocode:
> If that operation requires the contents of the file, then the program cannot continue unless it successfully reads the contents of the file. (...) If you cannot open the file, there isn't any more to do.
This is just a regular "if-else" that can be done with any programming language. The behavior of your program when the file cannot be opened is part of the specification; just as its behavior when it can be opened. I agree with you on that, and I add that the desired behavior can always be implemented using regular control flow constructions. You do not need a specific language construct for "errors", as you have proven by the algorithm that you have described in your text.
> stop doing things
This is what we call raising an exception.
> I add that the desired behavior can always be implemented using regular control flow constructions.
I agree. But that's not a very interesting observation. We added language specific constructs for errors not for the computer but for the human. These constructs make reading and writing code easier and safer.
A code with if-else constructs for every possible error condition is really hard to follow and very brittle. If the result of every error condition is the same (stop doing things) we have developed constructs to make that path easy.
The problem with errors is that understanding and resolving them is often non-local. If a network call fails the code where that fail happens doesn't have enough information to resolve it. If the solution is re-try the entire operation 3 times and then give up, the handling of this error must happen back where the operation starts not some random place in the middle where it actually occurred. Maintaining all the if/else necessary to move that information up through potentially dozens if not hundreds of calls is extremely difficult. And, it turns out, completely unnecessary and easily automated.
Ok, then replace all additions in your program with a function returning either an error or the result. Same with logging statements. And don't ignore the errors.. Is your program still readable?
I agree. You don’t open a file, you try to open a file. When you get a handle back, that’s your library skipping steps.
Can you name a language/ecosystem that gets it right?
Rust seems pretty close to this (with Result<T, E>), though there is also the panic system.
it's trivial in any language to define and use a Result<T, E> type.
4 replies →
Though I don't think it's perfect, languages like Go that treat errors as simply another type and you check as a return value do get closer to treating errors and success symmetrically versus an exception throwing and typing system like some other languages.
Building on that, the ADT languages with full option types here are fantastic here, because the shape of the data in each case can be taught to the compiler.
Erlang / OTP.