[Haskell] Better Exception Handling

John Goerzen jgoerzen at complete.org
Tue Nov 23 11:20:15 EST 2004


On Tue, Nov 23, 2004 at 03:43:09PM -0000, Bayley, Alistair wrote:

Thanks for your thoughtful reply.  Let me try to expand on it a little
bit.

> Here's how I create "custom" exceptions; it doesn't seem onerous to me, but
> then I have a high tolerance for some kinds of coding pain:
> 
> > data SqliteException = SqliteException Int String
> >   deriving (Typeable)
> 
> > catchSqlite :: IO a -> (SqliteException -> IO a) -> IO a
> > catchSqlite = catchDyn
> 
> > throwSqlite :: SqliteException -> a
> > throwSqlite = throwDyn

That works well enough if your code is limited to Sqlite exceptions.
But if you are at a higher level in the code -- you might be handling
Sqlite exceptions, or maybe IO exceptions, or maybe some other
exceptions -- it gets more difficult.  You have catchSqlite to use, then
there are the System.IO.Error catch/try functions, and then of course
there are the Control.Exception catch/try functions, which have the same
name but don't do the same thing.  If you are using any other toolkits
too, you may have more of a problem yet.

> > Python can work that way, but also adds another feature:
> > 
> > try:
> >     blah
> >     moreblah
> > finally:
> >     foo
> 
> And in Haskell we have catch(Dyn), bracket, and finally. Are these not
> enough?

I hadn't been aware of finally.  That does seem to help.

> Does it? I'm not convinced... I think it's no more verbose than any other
> exception-handling mechanism, but maybe there's some cognitive overhead in
> translating an OO exception-handling idiom into Haskell. All I've ever used
> is catch and bracket, and I find them fairly straightforward. Could you post
> some code which you think would be clearer with an Ocaml or Python exception
> handling style?

Sure.  Here's a Python example... this isn't necessarily completely
accurate code, but it should be close:

import ftplib, os, sys

f = ftplib.FTP("ftp.kernel.org")
try:
    try:
        f.cwd('/pub/linux/kernel/v2.4')
    except (ftplib.error_perm e):
        print "I can't access the directory", e
        sys.exit(2)
    f.retrbinary("RETR ChangeLog-2.4.13", 
                lamba block: sys.stdout.write(block))
    f.quit()
except (ftplib.error_perm e):
    print "Permissions error ", e
    sys.exit(2)
except (ftplib.error_temp e):
    print "Temporary error, please try again later", e
    sys.exit(1)
except (ftplib.all_errors e):
    print "Other FTP error", e
    sys.exit(2)
except e:
    print "Non-FTP error", e
    sys.exit(3)

The various ftplib errors, when printed, will show the error code and
message from the server.  In a more complex program, these can
definately make a difference; for instance, if you get a permissions
error when trying to get a directory listing, you may treat it
differently than a permanent error (some directories exist but cannot be
listed, so it's a different situation.)

So, in Haskell, we would have to define a way to communicate what class
of error we have from FTP.  Perhaps a data type that could be used with
throwDyn.

Next, for our outer loop, we'd have to do something like:

exctest :: FTPExc -> IO a
exctest e = case e of
   ErrorPerm x -> foo x
   ErrorTemp x -> bar x
   _ -> "other ftp error"

Then, we'd have to be able to deal with the non-FTP exceptions.  So, if
I'm getting this right, the handler code would look something like this,
where dlFile is the file downloading function:

catch (catchDyn dlFile exctest) (\a -> "non-ftp error" ++ show a)

It works, but it's not all that great, and it would be particularly
nasty if I tried to write it out inline, although that makes the code
most readable.  It would be even worse if I needed to handle several
different types of Dynamic exceptions, since I'd have to have a separate
handler function and a separate catchDyn for each.  (Or, I'd have to
just accept the Dynamic from catch, and handle it manually with
fromDynamic, which is not that much more pleasant and certainly
non-intuitive.)

Part of what I'm getting at here is ease of using exceptions and
maintaining code.  I've seen a lot of Haskell code, from a lot of good
Haskell progammers, that just uses fail/error with a certain pattern in
a string because it's easier than going through the Dynamic mechanism.

> > The other annoying thing is forcing it to run in the IO monad. 
> 
> Note that you can throw exceptions from anywhere. They just have to be
> *caught* in the IO monad.

Right, I got that.  My point was that if a function has deterministic
failure, as in my (String -> Int) example, I'm not certain why it has to
be that way.

-- John


More information about the Haskell mailing list