As for most computer scientists, I always think about the features I would include into a programming language. Though I will probably never actually implement one (it is a lot of work, and there are plenty of languages already there), thinking about it can lead to interesting questions and insights. One such insight is that any practical language should have an exception system – which, however, should only be used in actually exceptional situations.
In theory, everything is embeddable in an Exception monad (or a sum type). But some exceptions can always happen, for example OOM. Or a system signal.
There are lots of algorithms that only work when everything goes right. A parser that returns a cototal AST, which is consumed lazily by an algorithm, for example. (Update: Seems like for such cases, Iteratees are better-suited.)
Verification of such algorithms can be tedious when one always has to cope with the possibility of failure. Abstracting very uncommon failures away into exceptions should be possible in a type-safe way. However, of course, they bloat the type system. Therefore, an Exception-system should mainly contain the possibility to safely unroll the call-stack and return the program into a safe and well-defined state.
For example, Lisp's way of communicating across the borders of stack frames and defining restarts is elegant and well-suited for rapid prototyping, but too general for formal verification. Such constructs should only be used with sophisticated monads, or given as parameters, but do not really fit into strictly typed languages, I think.
Exceptions should only be thrown in exceptional situations. I worked out the following three three criteria:
The problem must be something unlikely that usually does not happen.
Not unrolling and aborting would leave the program in an indefinite state.
The program cannot really do anything about it, or at least doing anything about it is not in the domain of the current subroutine.
These are not sharp criteria. There are many cases where it is debatable. In the end, they are more guidelines for API design. I came up with these when thinking about several examples:
The filesystem gets out of memory: While writing a file, that is
something unlikely. One should close and maybe delete the file
afterwards. The program cannot do anything about it, since this is
something the system has to cope with. However, when using
fallocate(2), returning an error is a well-defined behavior, and
should not result in an exception.
The program runs out of memory: Also something unlikely with
sophisticated memory management. However, this exception may be
thrown by custom allocators, but not by explicit calls to
malloc(3) and the like. Because
well-defined behavior. If your custom allocator cannot allocate,
then it may throw an exception.
malloc(3) itself or similar raw
abstractions over it should not.
End-of-File while reading: Files are usually finite. This is nothing unexpected. It can, of course, be unexpected when parsing something. But then, this is a parser exception, not an exception about the EOF itself.
If you have any opinion about it, feel free to let me know.