Advertisement Jump to content
Sign in to follow this  
ApochPiQ

Towards better error handling constructs

This topic is 1804 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

We all know about the eternal war between the Big Two error handling philosophies.


From Ages Immemorial we have the return-code method:

enum ReturnCode
{
    RETURN_CODE_SUCCESS,
    RETURN_CODE_BARF,
    RETURN_CODE_VOMIT,
};

ReturnCode MyFallibleFunction(int param1, const std::string& param2)
{
    // Do stuff

    // Ooops!
    return RETURN_CODE_BARF;
}
This works fine if your function only returns a result code; if it needs to have a payload as well, then you're stuck with out-parameters or other nasty hacks.


Therefore, from Slightly More Modern Times we have the exception method:

void MyFallibleFunction(int param1, const std::string& param2)
{
    // Do stuff

    // Ooops!
    throw BarfException();
}
This is nice because you can return the payload you want normally, and even attach arbitrary additional state data to the exception object itself. Unfortunately, exceptions are complicated and hard to get right, even in managed languages. Implementing exceptions is a major burden on language designers as well.


Of course there are other methods in use, such as multiple-return (return a payload in one "slot" and a success/fail code in another slot - popular in Lua, Go, etc.), discriminated-union-return (return an algebraic sum type that can carry either a success result or a failure code - popular in several functional languages), and so on. There's even the ever-mystical "continuation" method which is something like exception-style stack unwinding on steroids. (Check it out if you're not familiar with continuations, they're a damned powerful but really tricky concept that can be, at turns, very nice to have and infinitely frustrating to debug.)

A notable alternative that is popular in JavaScript (and probably other similar languages) is lambda-style handlers wherein I pass a function two lambdas, one that runs on success, and one that runs on failure. This is kind of a nice idea, but it turns into a soup of nested lambdas in complex scenarios, and gets ugly really, really fast.


I was thinking earlier about how I would implement error handling in an ideal scenario, and came up with a list of mandatory functionality I'd want involved:
  • Let me return a payload trivially - and with zero runtime overhead - in success cases
  • Allow arbitrarily rich "failure" objects/codes/etc. so I can be very precise about what went wrong
  • Complete static type safety
  • Allow arbitrary handling logic from the caller or callee when errors occur
  • Keep error processing logic close to, but not intermingled with, success-case code
Some of these are obviously easier than others.

Here's what I came up with:

entrypoint :
{
    protect
    {
        print(FallibleFunction(42, "Test"))
    }
    with task
    {
        Barf : { print("Function barfed.") }

        Vomit : string reason -> integer fallback = 0
        { print("Vomit! Falling back to 0 becuase: " ; reason) }
    }
}

FallibleFunction : integer p1, string p2 -> string ret = ""
{
    if(p1 > 100)
    {
        panic => Barf()
    }
    
    while(p1 > 20)
    {
        p1 = (panic => Vomit("Number slightly too high"))
    }

    ret = p2 ; " ... " ; cast(string, p1)
}

In this example, we set up a "fallible" function which accepts an integer and a string. We then call this function from inside a "protect" block, which is followed by a task (think actor) which can accept messages.

(NB: think of foo=>Bar() as syntax for "send the Bar message to the foo task." In this case, panic is a special task alias for the "nearest" protector task that can handle the given message.)

Inside the function, we check if the integer passed is "very large" in which case we just "barf." This is equivalent to an unrecoverable error. Then, we set up a recoverable error if the integer is "slightly" too high. In the recoverable case, we actually fire a message to the protector task and use its return value to change the parameter. This repeats until a sane value is passed in; for sake of simplicity, this can loop infinitely, but more complex and realistic handling would just obscure the example.

Finally, the function constructs a string based on its input parameters, and returns it.



I like this mechanism. It allows trivial returns of payloads in success cases, and even allows totally unguarded execution, similar to exceptions and stack unwinding, if I (as the programmer) so desire. Failure messages can pass arbitrarily rich details to the protector task. Enforcing type safety and even static error-robustness checking is possible, albeit not necessarily trivial. We see a perfect example of caller and callee interacting to correct the "vomit" condition. Last but not least, I could hypothetically use a non-inline task if I wanted to, allowing reuse of error handling logic, or separation of concerns between success and failure paths, etc.

This feels to me like the best of all possible worlds, but I'm curious if it even makes sense to anyone else, or if someone has a better idea for how to handle error situations in code. Keep in mind I'm not looking for a solution to bolt into an existing language so much as a theoretical ideal.


Thoughts?

Share this post


Link to post
Share on other sites
Advertisement

This feels to me like the best of all possible worlds, but I'm curious if it even makes sense to anyone else, or if someone has a better idea for how to handle error situations in code. Keep in mind I'm not looking for a solution to bolt into an existing language so much as a theoretical ideal.
Thoughts?


Once I grok'd the syntax (Epoch?) the concept certainly made sense although the first concern which popped into my head was noise at the call site.

In fact, going back to your opening, one of my 'issues' with exceptions isn't so much the burden of handling as it is that you end up often wrapping up all the code in a try {} block instead of just the bits you think will fail as it saves you jumping in and out of the block and mixing try{} catch{} groups across a function.

I can see the same happening here, where everything ends up in the 'protect' block and an ever expanding list of 'task' to clean up on the end.

Working from that premise maybe assume always protected and setup the error handling via some other means?

entrypoint :
{
    print(FallibleFunction(42, "Test"))
}
with handlers
{
    Barf : { print("Function barfed.") }

    Vomit : string reason -> integer fallback = 0
        { print("Vomit! Falling back to 0 becuase: " ; reason) }
}


FallibleFunction : integer p1, string p2 -> string ret = ""
{
    if(p1 > 100)
    {
        panic => Barf()
    }

    while(p1 > 20)
    {
        p1 = (panic => Vomit("Number slightly too high"))
    }

    ret = p2 ; " ... " ; cast(string, p1)
}
Although this does introduce scoping issues for variables and the inability to 'catch and recover' within a function which might prove an issue, I'd have to ponder that some more. Although depending on the nature of the agents and how they bubble errors up this might not be a problem - it might not be logical to 'catch and recover' within in the semantics of the error handling agents and the rest of the program flow.

Overall however, I do find the idea of separating out error reporting/handling in this manner an interesting idea. Edited by phantom

Share this post


Link to post
Share on other sites

I feel like that sort of syntax would lead to a very tight coupling between the function and the protect block.  You'd have to know within a function, who would be calling it, and the calling functions would have to know all the possible 'exceptions'.  Having to specify all the 'exceptions' isn't hard for a single function, but once they start getting nested... 

 

It doesn't seem practical to me.  Outside of trivial examples I can't for the life of me think of a good example.  Even in the case of say an invalid file name in a file handling function, looping on an exception or sentinel return value would be easier.  This also looks to me to be a maintenance nightmare.

 

Clever, but I question its practicality.  But just cause I can't see its use doesn't mean there isn't one.  Any good examples come to mind?  Ones that wouldn't be trivial to code using exception handling or sentinel return values?

Share this post


Link to post
Share on other sites

I'm sorry I disagree Frob, I use exceptions almost exclusively, and it'd be a cold day in hell before I returned to return values.

 

Both return values and exceptions have the same amount of error handling code.  The difference is with return values you must handle it at the function return point, with error code spread all over the place.  With exceptions you can handle it where ever is the most convenient/easiest/least error prone to do so.

 

A proper exception hierarchy will also provide much more information, and be quicker and safer to expand, than an error code.  Add a new error code to a function and anything that uses that function will have to be re-written.  Add a new exception and only a few catch() blocks need to be updated.

 

Ignored exceptions are also easier to track.  Ignored return value errors can cause the program to execute significantly past the point of error, corrupting memory along the way and making tracing the bug tedious.  An ignored exception will halt the program, with a very informative stack output and all data up to the exception intact.

 

I also don't see why constructors throwing exceptions are a bad thing.  That was one of the primary situations that exceptions were created for.  Obviously exceptions in destructors are bad, but its not like you can return a return code from them either.

 

I know exceptions seem to get a lot of hate, but once I got used to them I fell in love.  Its one of the few things I love about C++...

Share this post


Link to post
Share on other sites
What you seem to be describing is a "warning" system that works in place of the "error" system that is exception. That is, you're creating a mechanism to continue on that's controlled at the call site. That's not really one of the 5 priorities you listed.

I think exceptions are much better than return codes, precisely because you can put the code to handle the exception case out of line of the normal path.

But exceptions do have their problems too. For example, sometimes you do want to handle the exception right at the call site, but exception syntax is very verbose for doing that. So you've traded brevity for flexibility.

Share this post


Link to post
Share on other sites

What you seem to be describing is a "warning" system that works in place of the "error" system that is exception. That is, you're creating a mechanism to continue on that's controlled at the call site. That's not really one of the 5 priorities you listed.

I think exceptions are much better than return codes, precisely because you can put the code to handle the exception case out of line of the normal path.

But exceptions do have their problems too. For example, sometimes you do want to handle the exception right at the call site, but exception syntax is very verbose for doing that. So you've traded brevity for flexibility.

 

But is the syntactic difference really that much?

a = GetNumber();
if (a == error_1) { /* handle error 1 here */ }
else if (a == error_2) { /* handle error 2 here */ }
else { /* handle everything else here */ }

vs

try { a = GetNumber(); }
catch (const Error1& e) { /* handle error 1 here */ }
catch (const Error2& e) { /* handle error 2 here */ }
catch (...) { /* handle everything else here */ }

I mean, I understand that prior to exceptions that the return value code would be clearer simply due to familiarity.  But having used both, from a purely syntactic point of view, the difference is negligible.  Only in the most simplest of cases are return values smaller:

if (!GetNumber()) {}

vs

try { GetNumber(); } catch(...) {}

That's about the only situation I can think of where return values are clearly tidier.

 

Perhaps there are situations I'm not aware of?  When you say exception handling syntax is verbose what in particular are you thinking of?

Share this post


Link to post
Share on other sites

A thing I do in my code since I've switched to exceptions is to take advantage of the following property:

  • many places in which stuff can go wrong;
  • usually one place in which we can trivially say "from now on, nothing can go wrong"

So, I push on stack a sequence of "undo" functions and disable them when successful. It's still far from the "fire and forget" construct I'd like but still better than writing an explicit try/catch in my opinion. The lack of error information however is a real concern, I suppose I could work out an half-decent interface for it... but in the end, I don't need that so my research ends here.

Continuation appears interesting; I have a testbed for this, maybe in the future I'll investigate.

For now, I'm following the thread.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!