5
\$\begingroup\$

I have a set of domain objects which try to find a solution to a problem.

The top level object has an algorithm which splits up the problem and delegates it to lower level objects, which in turn do the same.

I want the top level algorithm to throw an exception if it did not find any solutions, giving a breakdown of possible causes.

My question is, how should(n't) this be implemented?

The way I was thinking of doing this:

The algorithms at all levels throw an exception if it fails to find a solution.

For a given level, call it the parent algorithm, it would accumulate exceptions thrown by its children, and if all attempts by its children throw an exception, the parent will itself throw an exception which contains its childrens' exceptions.

My implementation is as follows:

class ExceptionWithCauses extends \Exception {

    private $causes = array();

    public function addCause(\Exception $ex){
        $this->causes[] = $ex;
        $this->message .= "\n".$ex->getMessage();
    }

}

I'm using it like this:

function findAnOption()
{
    $noOptionsException = new ExceptionWithCauses("findAnOption(): No options found");

    $possibilities = array(1, 2, 3, 4);
    foreach ($possibilities as $possibility)
    {
        try {
            useOption($possibility);
            return $possibility;
        } catch (Exception $e) {
            $noOptionsException->addCause($e);
        }
    }
    throw $noOptionsException;
}

function useOption($x)
{
    if ($x < 5){
        throw new ExceptionWithCauses("useOption(): $x is too small");
    }
    return $x;
}

And when findAnOption() is called, a exception is thrown with the following message:

findAnOption(): No options found
useOption(): 1 is too small
useOption(): 2 is too small
useOption(): 3 is too small
useOption(): 4 is too small

I'm worried that this is bad practice. I've read that exceptions should only be used in exceptional circumstances. So with that in mind, perhaps I should only be throwing an exception at the top level. But then at that top level, how should the exception accumulate details on what was tried by lower levels? The only time that information will be needed is when the top level throws that exception, so in a way it makes sense that it's all handled by exceptions, and that I have exceptions at every level?

On the one hand, this question on Programmers SE explains why using exceptions in control-flow is an anti-pattern, but at the same time, it seems like the tidiest way of accumulating a log for why a top level exception occurred?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ > using exceptions in control-flow is an anti-pattern Exactly that, only if it is used for the sake of control flow, and not exception handling. so if you throw an exception for your caller when there was nothing wrong, but just to make the caller branch to different logic, then this is an anti-pattern. \$\endgroup\$
    – Bishoy
    Oct 19, 2015 at 21:25
  • \$\begingroup\$ I think you have simplified the problem to the point where exceptions are no longer needed. I could understand if this was a very high level entry into some very deep 'algorithms' why exceptions would be the attractive and nicer solution, depending on how rare it's expected for an algorithm to fall (given the overheads of stack trace creation) \$\endgroup\$
    – Ryan Leach
    Sep 21, 2019 at 7:07

2 Answers 2

4
+50
\$\begingroup\$

I'm agreeing that it is bad practice, and that you should use exceptions to indicate error situations. Letting exceptions propagate including other levels of detailing sub exceptions, seems like something you'll loose control over quite soon.

I would rather extend the different functions to return the needed information either directly through function returning result objects which could hold the detailed information needed at the top level to determine the causes. You would then collate, and keep on returning more information up the chain.

Or you could implement a message queue where the delegates publishes messages regarding discrepancies onto the queue, and you have a central subscriber collating all those into some sensible report to present at the end. This would be especially useful if you are talking about higher level parallell algorithms, but might be overkill for a simpler system. It does however have the advantage that code at any level, could post a message and it's kind of separate from the code logic elsewise.

\$\endgroup\$
4
\$\begingroup\$

The other way you could do this, noting that you don't actually use the return value of useOption, is to have it return an error message or false, along the lines of:

function findAnOption()
{
    $problems = array();

    $possibilities = array(1, 2, 3, 4);
    foreach ($possibilities as $possibility)
    {
        $problem = useOption($possibility);
        if ($problem === false) return $possibility;
        $problems[] = $problem;
    }
    throw new SomeExceptionType($problems);
}

function useOption($x)
{
    if ($x < 5) {
        return "useOption(): $x is too small";
    }

    // No problems
    return false;
}

The exception approach can certainly be clearer, and I wouldn't discard it just because it's "using exceptions for control flow". Exceptions are always control flow: the question is whether they're the appropriate one for a given case.

However, I would make one change: I prefer the name AggregateException (stolen unashamedly from Microsoft's .Net API) to ExceptionWithCauses.

\$\endgroup\$
1
  • \$\begingroup\$ I mean, Sure if you want to create a result object, then act on the result, then you can keep the context with the result. But it's much easier to get away with this in dynamically typed languages as you don't need to deal with as much boilerplate. Checking for 'false' just feels like garbage in, garbage out however. \$\endgroup\$
    – Ryan Leach
    Sep 21, 2019 at 7:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.