Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I'm not sure his example for "exceptions are too complicated" is very good. When deliver mail, the operation isn't "first try the primary server, then try the backup server". It's "try the servers in ascending order of priority, randomly selecting if the priorities are the same". From that, the code becomes much simpler:

    def servers := ( sort possible servers according to rules );
    def sent_to;

    send_attempt: for server in servers {
        try {
            server->send_mail_to( message );
            sent_to := $server;
            last send_attempt;
        }
        catch Temporary Failure {
            logger->note_temporary_failure( ... )
        }
    }

    if(!sent_to)
        throw Permanent Failure ("EPIC FAIL");

The key is to use exceptions for what they're good for: unwinding the stack under certain conditions. We want the stack to unwind if there is some reason to abort mail sending completely ("network cable not plugged in"), but we only want to unwind to the point of trying the next server if the server is simply unavailable.

Using error codes alone would be complicated ("if result_code == TRY_AGAIN_LATER then { try the other server } otherwise { return TOTALLY_FAILED_DUDE }"), and using exceptions without the "sent_to" state would also be quite complicated (I'm not really sure how to even do that).

The key is to use the features for what they're good at, rather than to treat them like an ideology.

From an ideology perspective, your language's designer already decided what he wants you to do. If you can accidentally ignore an error, you're using the wrong one.

That means in Java, you tend to favor exceptions, because you get some compile-time type checking to make sure you're handling them. And if you don't handle them, your program exits at runtime, which is a good thing. Return codes, on the other hand, are easy to ignore, and your program produces undefined results if you forget to check them (and no compiler is going to tell you where that is: you'd better have good tests, tolerant users, and no deadline).

In Haskell, on the other hand, the ideology is reversed. If you throw exceptions in IO, they basically work like exceptions in untyped languages -- your program exits unless you were lucky enough to have a runtime handler. (If you throw them outside of IO, like with "error", you're double fucked. You can't even catch them outside of IO, which basically means Never Do That.)

But the good news is, there are type-safe return codes. You define a type like:

    data Either a b = Right a | Left b
and then you make your functions return Either the Right answer or an error. If you have a function that returns an error code, you can't do anything with the result until you intentionally handle the error. If you forget, your program simply does not compile -- there's no way to ignore errors except by explicitly writing code to ignore the error.

And, of course, this is generally hidden from the programmer with monads! You don't even have to think about handling the return codes: the language does it for you.

(C is a weird case where both exceptions and error codes work poorly. For that reason, I make sure to write C in very tiny pieces that can be composed with a safer high-level language. In that case, there are only a few places where errors can occur, and they tend to be simple like "tried to allocate memory, it didn't work, so i deallocated everything else" or "failed to send to the socket: EAGAIN". The complex logic like "failed to send to primary MX" should be handled higher up in your stack.)



Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: