IMO, there’s not much difference between the two. When I think about exceptions, I first think about how they should be handled. For example, if an exception signals an unexpected external error, such as running out of disk space, pretty much the only thing you can do is to show an error message (perhaps not even log it, since there’s no free space). There are also expected exceptions that shouldn’t even exist in a well-designed programs, but sometimes you just have to do it. For example, a 3rd party library may throw an exception in a situation that is well expected in your software. In such cases, it makes sense to catch that exception as soon as possible and convert it into something more useful for flow control, such as a result-like object.
But when it comes down to things like IllegalStateException
and IllegalArgumentException
, they usually indicate a bug in software. It doesn’t really matter what kind of bug it is, as long as there are enough details in the error message. There are few exceptions to this rule, such as NumberFormatException
, but that’s just bad library design.
Here’s an example that blurs the line between them even further. I designed a result-like class Outcome
to avoid abusing exceptions and to have a neat way to organize code around expected errors. Sometimes, however, you know in advance that a certain call can’t possibly produce an exception (for example, Integer.parse(s)
would never throw if you pass in a string that was previously matched against \d{3}
). So I eventually came up with a test like this:
val result: Outcome<MyClass> = MyClass.parse(s)
val value: MyClass = resut.get()
Now, parse()
can return either a failure or a success. And get()
must throw an exception if called on a failure. What exception? Well, it’s an indication that the state of the result is wrong, so it must be an IllegalStateException
, right? Seems so. But here’s another example:
fun tryParse(s: String): Outcome<MyClass> = // ...
fun parse(s: String): MyClass = tryParse(s).get()
Now I have two functions: one should be used when I don’t know in advance whether the argument is valid, the other one is to be called I do know that. But with an implementation like this, parse(s)
will still throw an IllegalStateException
on an invalid argument! This doesn’t make any sense because the Outcome
instance is now purely an implementation detail of parse()
. So, should I catch the exception in parse()
and convert (or wrap) it into an IllegalArgumentException
? One would think so, but it would just lead to unnecessary boilerplate while providing little to no value to the API user. Even if I were to encapsulate that boilerplate into something like getOrThrowIllegalArgumentException()
and getOrThrowIllegalStateException()
that would still be rather ugly.
Eventually I just decided to let IllegalStateException
to propagate, and just never write catch (IllegalStateException)
or catch (IllegalArgumentException)
.