• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
ApochPiQ

Towards better error handling constructs

11 posts in this topic

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?
1

Share this post


Link to post
Share on other sites

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
2

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?

1

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++...

0

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.
0

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?

0

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.

1

Share this post


Link to post
Share on other sites

How is that different to throwing Barf() or Vomit(string reason) from a function?


Once you throw an exception, you're abandoning a call frame and all of its context. This allows callers to interact with the erroring call frame to recover.

 
 

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.


Perfectly true, and not something this would solve, unfortunately. I'm not sure of a good way to address this in general. The best I can offer is that you don't have to put the protection task inline; it could be defined as a standalone entity someplace else and just used by name:

protect
{
    // Fallible stuff
}
with PreExistingHandlerTask
 
 

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?


Fair points. It would certainly be open to abuse and creation of really nasty spaghetti logic, which is somewhat counter to my ideals.

 
 

Is error handling even a big deal?


It depends on what you consider an error, I guess. This is mostly useful around interface boundaries where you can't assume everything is always validated already. Suppose you want to validate a ton of user-generated data and then provide feedback to the user on how to correct any problems, such as what a compiler does. I consider this error handling, although that terminology can easily be debated.

 

It might just be because it's an unfamiliar construct, but I'd probably prefer that Vomit just be a lambda, which is invoked when the input is too high and returns a replacement value.
It's also not obvious to me why Barf is unrecoverable (halts the program?) but Vomit isn't? Is the idea that handlers with no ret-val are basically crash handlers, but ones that have a ret-val are lambdas?


Barf is not unrecoverable. It's specifically guarded against by the protector task. If I introduce a third error, such as Spew, which is not in the protection task, then it bubbles up the call-stack exception style, or terminates the program if nobody catches it.

My gut reaction against just passing lambdas around is that this turns into really deeply nested lists of lambdas in my experience. For instance, "modern" JavaScript - especially in non-browser contexts - is often many nested levels of lambda passing, sprinkled with passing functions by name instead of inline to avoid the syntax from becoming a (bigger) mess.
1

Share this post


Link to post
Share on other sites

Barf is not unrecoverable. It's specifically guarded against by the protector task. If I introduce a third error, such as Spew, which is not in the protection task, then it bubbles up the call-stack exception style, or terminates the program if nobody catches it.

Sorry, I was thrown off by your terminology in the OP:

"barf."... is equivalent to an unrecoverable error.

So Barf is basically a regular exception -- control is 'thrown' from the panic site, to the "catch"/task, then back to the end of parent protect block, and resumes from there?
But Vomit is an "in-line exception" -- control is thrown from the panic site to the task, but because the task has a return value, it resumes from the panic site (instead of resuming from the end of the parent protect block)?
Is the lack/presence of a return value the mechanism that differentiates the two cases?

 

What happens when I write

panic => Vomit("Number slightly too high")

instead of:

p1 = (panic => Vomit("Number slightly too high"))

 

Vomit has a return value, but I'm ignoring it. This causes the throw/panic to look like an "unrecoverable error" where control will bubble up the stack, but will it actually be a recoverable error (basically a lambda) because Vomit returns a value?

If so, you'd ideally want to catch unused ret-vals as a compile-time error wink.png

Edited by Hodgman
2

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0