You can still readily express the concept of conditions in Rust, but it’s no longer baked into the standard library.
Strong disagree with this sentence. It represents the exact opposite of what I deem a good programming language. The difference between "common" and "uncommon" execution paths must be of no bearing. Moreover, this "likeliness" depends on the (unknown to the programmer) usage that the program will be put through. For all you know, the uncommon path may be the only one that will be ever traversed in all instances of your program. A correctly specified program must deal with all possible input conditions, and that includes missing files, inconsistent parameters, and lack of resources. A filename existing or not is the same boolean value as an integer being even or odd.
Even outside of programming, human thought often works exactly in terms of general cases and exceptions. It's just what comes naturally, and we shouldn't be fighting it in code.
> The difference between "common" and "uncommon" execution paths must be of no bearing. Moreover, this "likeliness" depends on the (unknown to the programmer) usage that the program will be put through. For all you know, the uncommon path may be the only one that will be ever traversed in all instances of your program.
As the designer of that program, I obviously know up to a very good degree of confidence what the usage of the program will be: I am designing my program for a particular use, by definition. Sure, I may not know exactly how flaky your network may be, but I know that this program only really works on networks that deliver more packets than they drop, that can read all of the bytes I wrote to disk back, at least most of the time, and so on.
Or, perhaps I'm writing a program that is meant to run on very flaky networks (say, a device for wireless thunderstorm sensors): I expect that program to look very different from the Netflix app's networking code.
No; this is bad engineering. You write a program to conform to a specification. In the specification, it says what must happen when a file does not exist, what must happen when there's not enough memory, etc. Then you write the specified behavior into code.
I've never, ever seen a specification which mentions file-not-found or out-of-memory errors. The "specifications" I get from my clients are more like (paraphrased):
> Listen, adwn, I have all those files in a shitty, undocumented file format designed by another group in another building, could you please add an import button for them? Oh, and can I have it by Friday? Thanks, you're the best!
I'm exaggerating only slightly. And then I go and try to make sense of what they need, and I implement error handling on top of it.
Your specifications tell you exactly how to deal with file-not-found situations? You lucky bastard ;-)
And if the spec says "try downloading the file 3 times at 5 seconds interval; if that fails, give up the update", I am free to implement it however I want (?) unless I missing your point.
I've never seen an end-user application accompanied by a written a specification about what happens if there is a run-time error due to a mistake in the program.
Your original thesis is something like that "there are not errors, only conditions we care about"; but then this subsequent argument is disappointingly about environmental conditions only.
If I have a program that takes two run-time inputs and divides them, I may or may not specify what happens if the denominator is zero. I could explicitly specify that the behavior is not defined; my program can do anything. (In practice, any decent processor will catch it via some numeric exception.)
If my program generates a division by zero due to a programming bug, where no such division is implied by the specification of what the program does on the inputs which it is given, that's something we don't bother specifying. Not usually.
Anything can happen if the program has a mistake. A document listing all possible mistakes and how the program will react will not only be intractably long, but in the course of writing it, you would just inspect that the program doesn't have those mistakes. In the end, you'd be left with a document describing mistakes that the program doesn't actually contain, while it remains vulnerable to unknown mistakes.
We might be required to have a strategy for the software to deal with its own faults in a general way, if we are working on something safety-critical. Such as that no matter how the program might fail, the system as a whole will revert to a safe state.
Most software in business is written to implement a process. And business processes are exactly defined as a common workflow with edge cases and exception handling. Not all software is like this, but a hell of a lot of it is. And those business processes are not always complete, and they are typically changing over time as well. Any idea of a global specification outside of the process that the code is actually implementing becomes useless pretty fast.
For some domains, you can write a specification that has value in itself, it will be shorter and easier to review.
For other domains, the program itself is the best specification for the desired behavior. There is nothing special about a specification that makes it inherently more correct than a program. The specification may be large enough and detailed enough that it is essentially just as hard to review as the program output. The specification can have subtle bugs, just like a program. Even worse, the specification itself is harder to test, especially for corner cases.
It can be very easy to produce an excellent specification that solves a different problem than expected, especially for complex problems with many moving parts and many possible use-cases.
When people say "errors are values", I say: yes, but they are a special kind of values. The same way floats are special (signaling NaNs), or that bools are special (short-circuit operators). It is great to have special support for values which need to be used in a certain way (here: don't lose any error, bubble up by default).
It is great to have the opportunity (not the obligation) to use features for separation of concerns. This makes it a bit like an aspect-oriented approach where normal code emits errors and error handling code elsewhere knows how to handle/restart them. If you language allows it, it makes also sense also to decouple logging (tracing functions) or data-access permissions (postgresql row level security).
In the C code I maintain at work, everything is here in plain view, logging, error handling, etc., and I am not complaining, but more recently we tend to use other languages or code generation a lot (think of something like protobuf) because it makes code a lot easier to maintain.
Why? I think that a good programming language should help readers focus on the "essence" of an algorithm. Usually, handling stuff like memory allocation errors is just noise in that effort.
Besides, most errors will usually be propagated to the caller, so I think the programming language should make that convenient, like Rust does with its ? syntax, and most languages do (perhaps too implicitly) with some concept of exceptions.
Anyway, you can't just define the problem away, or else you end up with Go style error handling - that is exactly what a language with no built-in support for errors looks like.
Languages need to offer control flow mechanisms that allow you to separate common cases in the code from the uncommon cases. Especially since for most code, the uncommon cases appear deep in the bowels of an application, and the only way to handle them is to ask an actual human to handle them, usually on the opposite side of the application stack.
For a more reified implementation of this pattern, the Common Lisp Condition system is very interesting. It is similar to most Exception systems (code can raise a Condition, at which point a handler for that Condition is searched for up the call stack), but with one crucial difference: when raising a Condition, you can also specify Restarts - alternatives for how to proceed. Condition handlers can choose whether to cancel the execution flow and handle the condition with their own logic, OR they can choose to invoke one of the Restarts offered along the condition, based on their own logic. An example would be something like: Raise ConfigFileNotFound; Restarts: ContinueWithDefaults, Retry, ContinueWithOtherFile(newFileName). Then, a Condition handler (possibly a User) can choose to stop the application, or it can choose to continue with the defaults, or it can create the file with its own defaults and Retry, or it can try a different config file path.
This offers a lot of flexibility and has uses outside the idea of handling errors.