Steven D'Aprano <steve+comp.lang.pyt...@pearwood.info> writes: > On Thu, 02 Dec 2010 16:35:08 +0000, Mark Wooding wrote: > > There are better ways to handle errors than Python's exception system. > > I'm curious -- what ways would they be?
The most obvious improvement is resumable exceptions. In general, recovering from an exceptional condition requires three activities: * doing something about the condition so that the program can continue running; * identifying some way of rejoining the program's main (unexceptional) flow of control; and * actually performing that transfer, ensuring that any necessary invariants are restored. Python's `try ... finally' helps with the last; but Python intertwines the first two, implementing both with `try ... except'. The most important consequence of this is that the logic which contains knowledge about how to fix the condition must be closer to the code that encountered the condition than the resume point is. It's therefore hard to factor out high-level policy about fixing conditions from the relatively tedious business of providing safe points at which to resume main execution. (Effectively, each section of your program which wants to avail itself of some high-level condition-fixing policy has to provide its own protocol for expressing and implementing them.) Phew. That was abstract. Can I come up with some examples? I've been writing a (sort of) compiler recently. When it encounters errors, it reports a message to the user containing the position it had reached in the source, updates a counter so that it can report a summary at the end of the run and produce a sensible exit status, and then attempts to carry on compiling as best it can. The last bit -- attempting to carry on as best it can -- needs local knowledge of what the compiler's doing and what actually went wrong. If the parser expected to find a delimiter, maybe it should pretend that it found one, for example. The other stuff, printing messages, updating counters, and so on, is all done with some condition handlers wrapped around the meat of the compiler. That's written only once. Everything that signals errors, including standard I/O functions like open-a-file, gets the same treatment. (The remaining missing ingredient is a fluid variable which tracks the current position in the source and is updated by the scanner; bits of the compiler's semantic analysis machinery will temporarily focus attention on other parts of the source using locations they saved during the parse. Implementing fluids in Python can be done with a context manager: if you don't care about concurrency then you can use simple variables; otherwise it's little fiddly and the syntax isn't very comfortable, but it's still possible.) A more familiar example, maybe, is the good old DOS `abort/retry/fail' query. Implementing such a thing in Python, as a general policy for handling I/O errors, isn't possible. Viewed narrowly, this is probably a good thing: the old DOS query was very annoying. But the difficulty of implementing this policy illustrates the missing functionality. And, of course, if DOS had a proper resumable exception system, programs could have overridden the annoying query. In general, the code which discovers an exceptional condition may have several options for how to resume. It might be possible to ignore the situation entirely and carry on regardless (`false alarm!'). It might be possible to try again (`transient failure'). Alas, the logic which is capable of implementing these options is usually too low-level and too close to the action to be able to decide among them sensibly. (Maybe a human user should be consulted -- but that can drag in user interface baggage into a low-level networking library or whatever.) Resumable exceptions provide a way out of this mess by separating the mechanics of resumption from policy of which resumption option to choose. It's easy to show that a resumable exception system can do everything that a nonresumable system (like Python's) can do (simply put all of the recovery logic at the resume point); but the converse is not true. There are some other fringe benefits to resumable exceptions. * It's usual to report a stack backtrace or similar if an exception occurs but nothing manages to resume execution. If unwinding the stack is intertwined with working out how to resume execution, then whenever you /try/ to run an applicable handler, you have to reify the stack context and stash it somewhere in case the handler doesn't complete the job. This makes raising exceptions more expensive than they need to be. * You can use the same mechanism for other kinds of communication with surrounding context. For example, Python occasionally emits `warnings', which have their own rather simple management system (using global state, so it's hard to say `don't issue MumbleWarnings while we frob the widgets' in a concurrent program). A resumable exceptions system could easily integrate warnings fully with other kinds of conditions (and avoid the concurrency problems). You could also use it for reporting progress indications, for example. Of course, there's a downside. Resumable exceptions aren't the usual kind, so people aren't used to them. I'm not sure whether resumable exceptions are actually more complicated to understand: there are more named concepts involved, but they do less and their various roles are clearer and less tangled. (The `handler' for an exceptional condition can be called just like a function. Python doesn't have nonlocal flow control distinct from its exception system, but a nonlocal transfer to a resumption point isn't conceptually very complicated.) > 1. return a sentinel value or error code to indicate an exceptional case > (e.g. str.find returns -1); This works fine if you consider failure as being unexceptional. If you're not actually expecting to find that substring, and have something sensible to do if it wasn't there, a report that it wasn't there isn't really exceptional. (I think I use str.find more frequently than str.index, so it's nice that there are both.) > 2. raise an exception (e.g. nearly everything else in Python); Raising exceptions is a more complicated business than this suggests. Python made some specific design decisions regarding its exception system; they're pretty common choices, but not, I think, the best ones. > 3. set an error code somewhere (often a global variable) and hope the > caller remembers to check it; That's less common than you might think. Usually there's some sentinel value to tell you to look at the global error code. (Obvious examples where there isn't a clear sentinel: strtol and friends, math.h.) I think we can agree that this sucks. Someone else mentioned Erlang. An Erlang system is structured as a collection of `processes' (they don't have any shared state, so they aren't really `threads') which communicate by sending messages to each other. If an Erlang process encounters a problem, it dies, and a message is sent to report its demise. Erlang processes are very cheap, it's not unusual for a system to have tens of thousands of them. > plus some de facto techniques sadly in common use: > > 4. dump core; > > 5. do nothing and produce garbage output. > > What else is there? You missed `6. assume that erroneous input is actually executable code and transfer control to it', which is a popular approach in C. -- [mdw] -- http://mail.python.org/mailman/listinfo/python-list