1

Is this a known/common pattern? (example in Java)

class BadInput extends Exception {
    BadInput INSTANCE = new BadInput();
    private BadInput() { }
}

Whenever INSTANCE is thrown, it won't have a sensible stack trace but that's OK as it doesn't signify a coding error/resource issue/security issue/etc. (at a low level) but an input error (at a high level). Also, it won't explain why some input is bad. That's OK as well if no specifics are needed.

Here's a use case about (exact) integer division (it could just as well be about e.g. rationals or polynomials)

class ExactDivision extends BinaryOperator {
    int compute(int arg1, int arg2) throws BadInput
    {
        if (arg2 == 0 || arg1 % arg2 != 0) throw BadInput.INSTANCE;
        return arg1 / arg2;
    }
}

It may be involved in a (possibly complex) computation of some expression. If the exception is thrown, no cost is incurred in building a stack trace. All intermediate levels don't have the burden of having to check the validity of intermediate results since the exception simply propagates upwards (no checks as in if (!equalsErrorSentinel(subresult)) … are needed, which avoids both performance-penalties as well as coding errors (such checks are bound to be forgotten sometimes). At the end (at the top level), a caught BadInput exception simply means that the input makes the evaluation undefined. Absence of the exception means that the computed result is correct.

It seems to be a great pattern (only to be used when the two OK's above are really OK of course). Why have I never come across it?

Koen AIS
  • 81
  • 7
  • 1.) Why do you think it won't have a sensible stack trace? 2.) [When is optimisation premature?](https://stackoverflow.com/questions/385506/when-is-optimisation-premature) – jaco0646 Jul 04 '21 at 13:01
  • Because the stack trace is created at the point of execution where/when INSTANCE is created. It has no bearing with the point of execution where/when INSTANCE is thrown. – Koen AIS Jul 04 '21 at 13:05
  • Have you tested that? – jaco0646 Jul 04 '21 at 13:05
  • Ad 2) the use case is a simplification. The real use case involves exponentially many computations. – Koen AIS Jul 04 '21 at 13:06
  • So you've profiled the real use case, and generating exceptions is the bottleneck in the application? – jaco0646 Jul 04 '21 at 13:07
  • @jaco0646: Of course I profiled it (and it does significantly matter, even to the extent of differentiating between timeouts and solutions). Also I value the reduction of potential coding errors due to missed checks (although they could be fixed once found and not matter afterwards). – Koen AIS Jul 04 '21 at 13:15

1 Answers1

1

I've seen this sort of thing done for performance reasons in a few cases.

The usual way to do it, though, seems to be to override fillInStackTrace() The new implementation should just return this.

Both ways work, and you could even do both at the same time to avoid having a completely irrelevant stack trace in your BadInput instance. You should probably make the constructor public though, so people can still use the normal throwing code for your exception if they want to.

Also, if you're going to throw constants, I suggest having multiple constants like DIVIDE_BY_ZERO with appropriate messages built-in.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • Thank you. I like the overriding idea (just out of principle since indeed the stack trace need not expose anything at all). The constructor should remain private however. It has the sole purpose of quickly abandoning a computation without any other obligations. Perhaps in some contexts extending those obligations makes sense, but it'd require a good reason. PS: DIVIDE_BY_ZERO is besides the point: I don't care about the reason of failure to compute. – Koen AIS Jul 04 '21 at 13:58
  • OK, but you specifically called the exception `BadInput`, which assigns a reason to the failure, and an implication that it was an error. – Matt Timmermans Jul 04 '21 at 14:11
  • Yes: the reason the computation can't deliver a result is because it's "bad" but nothing more specific than that is needed. The actual reason really doesn't matter - at least not in my "real" use case - (perhaps somewhere down the line a division by zero was needed, or perhaps taking the square root of a negative number was required to yield an integer; it doesn't matter: evaluation fails and that's it). PS: the inputs are not user inputs but internally generated inputs (put roughly: millions of inputs and expressions are evaluated and all that is cared about is if they match) – Koen AIS Jul 04 '21 at 15:09