Consider a Product
with a quantity
which can be increased and decreased by a given amount
. The quantity should never become negative and if it is going to happen, the operation must be prohibited and the user warned.
public class Product{
private int quantity;
public Product() {
quantity = 10;
}
public void decreaseQuantity(int amount) {
int decreasedQuantity = quantity - amount;
if(decreasedQuantity < 0 )
throw new RuntimeException(String.format("Decrease quantity (%s) exceeds avaiable quantity (%s)",
amount, quantity));
quantity = decreasedQuantity;
}
}
For example if a product has quantity 10 and I try to remove 20, I throw a RuntimeException. SonarCloud suggests to replace the RuntimeException with a Custom exception, but I was wondering if there is a standard exception suitable for this case (Effective Java: Favor The Use of Standard Exceptions).
The most suitable exception seems to be IllegalStateException
. From the javadoc
Signals that a method has been invoked at an illegal or inappropriate time. In other words, the Java environment or Java application is not in an appropriate state for the requested operation.
and from Effective Java
IllegalStateException: This exception is used if, when a method is called, the state of the object is not valid for that operation. Potentially you have a filehandle and you call read before it has been opened.
However, it seems to me that there is a subtle difference between my example and what is assumed in the docs: it is not the state of the object itself that makes the operation illegal, it's the state of the object and the value of the input parameter. Reading usage examples (like What's the intended use of IllegalStateException?) the object is always in a state where the operation is rejected regardless of the input parameter.
There is nothing in the core java libraries that is a slam dunk. However, make your own exception is probably your best bet, it's a close race between that and using IllegalArgumentException
.
RuntimeException
Ill advised for nebulous reasons. The primary reason not to use it, is because linter tools will yell at you. The reason they yell at you is twofold, and to determine whether the right move is to tell the linters to shut up, or to head their advice, is to focus on these 2 reasons:
The type name of the exception is itself information. For example, throw new NullPointerException("x is null")
is dumb code to write (even though its common). It's superfluous - just new NullPointerException("x")
is appropriate. RuntimeException
conveys almost no information at all. You are mostly avoiding this trap: Whilst RuntimeException indeed conveys nearly nothing, the message of the exception says it all, therefore, the thing the linter is trying to stop from happening (throwing an exception that doesn't properly convey the nature of the problem) isn't happening, and thus you should consider just telling the linter to stop whining... except for:
The second reason you want a proper exception type is that you really, really don't want code that intends to catch this to have to do string analysis on an exception message. Thus, if there's any world in which you can foresee that some code wants to invoke your method and then react to the condition (of trying to subtract more than available) in any way other than blowing up all context, then you should be throwing an exception that means this specific condition and nothing else. RuntimeException
is never appropriate to catch if your intent is to catch a specific condition (it's pretty much never appropriate, period - sometimes you want to run code and react to any problem regardless of its nature, but then catch (Exception e)
is the appropriate catch block, or even Throwable
. e.g. appservers and the like should be doing this.
IllegalArgumentException
The linters won't be yelling, but you're still not really getting the secondary benefit (that is, allow callers to catch this specific problem and not some other problem with the arguments). It's also 'mentally' slightly dubious: Your reasons for initially disregarding it are not wrong. Generally IAEs are understood to imply that the illegal argument is illegal regardless of the state of this object. But this isn't written in IAE's javadoc and isn't universally applied.
So, downsides are:
catch (IllegalArgumentException e)
to deal with wanting to react to subtracting more than is there. IAE is still too 'general'.Upside is simply:
IllegalStateException
I think this is strictly worse than IAE. It's mostly the same story, except here the confusion is that ISE is usually used to flag that the current state of the object is such that the operation you are trying to call is simply not available. In a tweaked way that's actually true (the object's state is that there are 3 items; therefore it is not in a state that decreaseQuantity(5)
is an available operation right now), but it feels more confusing than IAE: ISE feels like the Product object itself is in an invalid state. I'd be assuming that the product is a legacy product that is not now or will ever be in stock again, or is some dummy product type (a Product object representing 'unknown product' or something similarly exotic).
But, same deal with IAE: The javadoc of ISE doesn't specifically spell out that you couldn't do it. Hence, if you would prefer to throw this, you could, it's not provably incorrect, at worst it's simply bad code style. A linter tool is never going to fault you for it, or if it does, the linter tool is wrong.
Advantages:
try {
product.decreaseQuantity(5);
} catch (QuantityInsufficientException e) {
ui.showOrderingError("Regretfully we don't have this item in stock at the quantity you want");
}
Is slightly more readable than catch (IllegalArgumentException)
, and more importantly, is more reliable: IAE is such an oft-used exception that you're going to one day edit the decreaseQuantity
method and introduce a code path that throws IAE for some other reason and now you have a very hard to find bug.
I'd be writing your own. Yes, it's a bit of a drag to have to write the source file, but you can let your IDE (or Project Lombok for maximum boilerplate busting) generate the entire file and you probably never have to look at InsufficientQuantityException.java
ever again.