A lot of code that's designed to convert or parse some data of type Foo
into a Bar
is written with the assumption that one wouldn't intentionally pass it invalid inputs. As such, it assumes that everything is correct and throws exceptions if it later realizes that something is wrong.
This is typically an appropriate thing to do; expect the inputs to be valid, throw if they're not. Unfortunately, there are some cases where the caller has a piece of data that could possibly represent different types. The simplest way to do so would be to try each of the possible parsers and use the output of whichever parser doesn't complain about it.
As a slightly forced but suitable example, let's say you're parsing incoming JSON data. As per some specification, a given string property can either be a number (as a string), one of a few magical keyword defined in an enum, or some other plaintext, each of which needs to call a corresponding add(int)
, add(MyEnum)
or add(String)
method.
The simplest way would basically boil down to
try {
add(Integer.parseInt(input));
} catch (SomeException e) {
try {
add(MyEnum.valueOf(input));
} catch (IllegalArgumentException e) {
add(input);
}
}
This blatantly goes against the oft-repeated mantra of "Don't control flow using exceptions", it falls prey to the dreaded performance cost of using try
and throw
, and it also goes against the notion that exceptions are only for, well, exceptional circumstances that typically shouldn't happen; here, we're basically explicitly saying "yep, there'll probably be an exception or two here before we're done".
Using parseInt
as the main example, a slightly prettier approach would be to create a utility method along the lines of
public static OptionalInt tryParseInt(String s) {
try {
return OptionalInt.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return OptionalInt.empty();
}
}
This is basically just hiding the exact same problems behind a pretty facade. Granted, it makes the calling code cleaner and it encapsulates the ugliness into a method dedicated to containing it, but it doesn't solve the underlying problems. It does however fit well into the "Keep it simple, stupid" rule of thumb.
A third option would to simply implement my own tryParseInt
from scratch. Unfortunately, that means I've got to duplicate well-established and likely well-optimized functionality in the JDK. In the more general case as well, if I'm using a library to parse my Foo
into a Bar
, it's likely because parsing it is complicated to begin with. So, this is a case of reinventing the wheel instead of reusing existing functionality, which almost certainly makes it a considerably worse solution in all but the most trivial of cases.
Option 4A is to write a tryParseInt
method that first validates the input, returning OptionalInt.empty()
if the validation fails, and passes the input to Integer.parseInt
if the validation succeeds. This has two drawbacks though. First, while not reimplementing the full functionality of Integer.parseInt
, it still performs the same validation that's done implicitly by the JDK method. We are avoiding exceptions altogether, but at the cost of having to essentially replicate well-established logic. The second problem is that it's not always easy to write completely correct validation without effectively writing a parser, thus...
Option 4B is to write optimistic validation logic that works in all the common and likely cases, but may still let some special cases slip through. The call to the underlying parser would then still be wrapped in a try-catch with the assumption that the catch will rarely be used. Again using tryParseInt
as an example, we could check if the string matches the regex [+\-]?\d{1,10}
to cover 99% of the cases. [2147483648, 9999999999]
and [-9999999999, -2147483649]
would still be let through, throw and be caught, but they're unlikely to ever occur. This also doesn't solve the underlying structural problems but prevents like 99% of the performance impact of having to throw an exception (assuming that the validation is fast).
Again, Integer.parseInt
and the rest are just simple examples of a more general problem when dealing with third-party parsers and similar.
Basically, my question boils down to how to compromise between the different, inherently incompatible, rules of thumb. Do we want to blindly abolish intentional exceptions altogether? Do we want to limit most of the exceptions' performance impact without adding too much complexity? Does simplicity and maximal code reuse outweigh those "nit-picky details" and "premature micro-optimizations"?
Is there a commonly accepted best practice for situations like this?