Proposal: Extensible exceptions

Henning Thielemann lemming at henning-thielemann.de
Sun Jul 13 16:37:00 EDT 2008


On Thu, 10 Jul 2008, Evan Laforge wrote:

>> As I said, in case of a bug, it is not possible to reliably clean up. That
>> an error is encountered proves that your assumptions about your program were
>> wrong, and so the assumptions about allocated resources are probably wrong,
>> too. To pick up Chris Smith's perspectives, cleaning up would be the task
>> for the next higher level, for which your error is only an exception - it
>> has hopefully kept track of the resources you allocated.
>
> How would the boundary where an error becomes an exception be defined?
> Would you have some catch error and rethrow as exception type
> function which defines the next higher level?  In my experience, the
> "level boundaries" depend a lot on the individual program, and are
> fuzzy because there are big boundaries and small boundaries.  Which
> ones you think of depend on how disciplined you're being and what
> level the code is at.

Within the same program you will have a very small number of such 
boundaries, say one or two. As far as I can judge, in GHC there is one 
such boundary. If the compiler encounters a strange behaviour of itself, 
it quits with 'Panic! report bug to ...'. I think that's the most one can 
do in such a case. Also a GUI driven application will have to quit in case 
of an error, maybe asking the user to save his data to a different 
location (not overwriting existing files, because the rescued data might 
be corrupted). A server will consider its plugins as a sublevel and should 
not crash, if a plugin crashes.

> Say I have (withFile something_broken).  I guess there's some kind of
> cosmic-ray chance that the broken code could somehow close the file
> itself and then withFile is in trouble (let's say close a file twice
> has some dire consequence), but the chance is low enough that there
> are bigger bugs to chase.  Does that mean withFile defines a level
> boundary that can reliably clean up its broken callee?

In my experience (with exceptions in an imperative language) I had cases 
where some cleanup routines caused crashes, but they were actually 
correct. They only crashed because something different went wrong before. 
It's then hard to find the real source of the problem - but let us aside 
this, since that's an debugging problem.

> It seems to me that there are lots of bugs that you can reliably clean up after.

It's certainly worth to explore that possibility, but its different from 
cleaning up in case of exceptions. Cleaning up in case of an error doesn't 
resolve the problem, that is, it doesn't fix the bug, but it may make you 
live more comfortable with the bug.

>> It seems that nobody except me is interested in handling specifically (up to
>> a certain level) the exceptional cases that an IO action can lead to. If I
>> see the type signature
>>  getLine :: IO String
>>  I have no idea, what kind of exceptions can occur. I even have to be
>> prepared to get OpenGL exceptions. Even error codes in C are more
>> informative in this respect. I would have to handle some exceptions which
>> look reasonable to me, and use a catch-all or rethrow-all mechanism for the
>> rest. Possibly the proposers of the extensible exception method like to rely
>> entirely on catch-all or rethrow-all just like errors. Wouldn't
>>  getLine :: ErrorT IOError IO String
>>  be much clearer?
>
> It think it would be interesting, though not really solving some big
> problem I've been having,

Examples?

> though sometimes problems are obvious only in retrospect.  Is there an 
> implementation to experiment with?

No sorry. It would be certainly worth to work out a counter-proposal to 
the currently discussed extensible exception proposal. If time allows it 
... It would at least throw a new aspect into the discussion of how to 
introduce "the" extensible exception modules. If there is more than one 
reasonable generalization then we should not try to replace the current IO 
modules with new ones, but provide both alternatives.

> But...  isn't it out of the scope of this proposal?  And I'm not sure
> how it fits in with the errors vs. exception distinction, or even what
> the api for errors would look like.  A parallel set of throwError
> catchError etc.?  Then it would just be a different kind of exception.

  Would you like to differentiate the ways your program crashes? I think 
division by zero, array index out of range and so on should all be handled 
by quitting the program and reporting the problem to the user, and 
eventually to the developer. Errors are indicated by "undefined", "error" 
or just by an infinite loop. I consider "error" to be a sugared version of 
an infinite loop. It's uniformly refered to as "bottom", isn't it? In case 
of an infinite loop you can also not cleanup, save user data or report 
anything to the user. Since referential transparency disallows distinction 
of several implementations of "bottom", a routine that handles "error"s 
differently from infinite loops must be a hack. But that's ok, because 
that "error handling" is actually debugging, and debugging is allowed to 
use hacks, just like "trace" is a hack, and an "error" that presents 
values as strings without having the Show constraint, is a hack, too.

  So, the "error handling" aka "debugging" system can be rather small:

indicating errors (I don't call it 'throw', differently from exceptions)
   error :: String -> a
   undefined :: a
   assert :: Bool -> String -> a  (runtime check that can be defined in terms of error)

In principle, a compiler flag could turn off the generation of code for 
'error', because a correct program would never step into 'error', and thus 
the flag would keep correct programs correct and buggy programs buggy. The 
flag would only increase performance and decrease debugging information.


   Debug.catchError :: IO a -> IO a

This is a hack that encapsulates a possibly buggy part of a program and 
cancels the abort started by 'error' and 'undefined'. It can of course not 
break an infinite loop or repair corrupted data. Maybe it should start 
another process or some OS sandbox in order to remain unaffected by the 
damages of the enclosed action. It will be needed for each layer of 
"conversion" from error to exception, say one or two times in a program.


   Debug.cleanupOnError :: IO () -> IO a -> IO a
   Debug.cleanupOnError cleanup action = ...

This routine runs 'action' and invokes 'cleanup' if 'action' is aborted by 
an error. After cleanup, it is again tried to abort the program by 
"error". If 'action' terminates regularly, no further action is taken, 
because it is assumed that 'action' could successfully free all acquired 
resources.
  There might be another routine that combines that with 'bracket' for 
normal exceptions, such that a resource is freed both if an exception 
occurs and if an error is encountered. Calls of this routine might become 
ubiquitous in a program.


More information about the Libraries mailing list