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