@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
@Incubating
@Repeatable(ThrowsIfConditions.class)
public @interface ThrowsIf
Represents a method's exceptional contract: the method throws the given exception when the given condition holds on entry.
@ThrowsIf(value = { b == 0 }, exception = ArithmeticException)
int divide(int a, int b) { a.intdiv(b) }
This is the exceptional counterpart of Requires: a precondition says
the caller must not do this (violating it is the caller's bug), whereas
a @ThrowsIf arm says this input is handled, by throwing — it is
defined behaviour, part of the contract, that callers may rely on (and
catch). The condition is a closure over the method's parameters, using the same
conventions as Requires.
Weaving. With the default woven = true the guard-throw is
generated at method entry — the general form of the pattern
groovy.transform.NullCheck provides for the null-check special case —
so the annotation is the implementation, not a comment about one:
if (b == 0) throw new ArithmeticException(...) // inserted; message derived from the condition
On a constructor whose first statement is an explicit super(...) or
this(...) call, the guard is inserted immediately after that
call, which must stay first — a language constraint.
With woven = false nothing is generated: the throw already exists.
Where it exists is the direct member — pure information for
readers and tools, with no effect on bytecode. direct = true (the
default): a hand-written throw statement lives in this body — checkable
documentation of a guard that is already there. direct = false: the
exception arises from code this method executes — a call, possibly
transitively (a third-party library), or a runtime operation — so there is no
throw statement in this body to find (and weaving a wrong specification about
invoked code would silently change behaviour, which is why such arms are
spelled woven = false). For woven code direct is implicitly
true and the member is ignored — most users never set it; a tool that cannot
find the promised throw in the body is the usual prompt to add
direct = false. The attributes are per-arm because real methods mix
modes:
@ThrowsIf(value = { x == null }, exception = NullPointerException) // woven for x@ThrowsIf(value = { y == null }, exception = NullPointerException, woven = false) // body guards y Object process(Object x, Object y) { ... }
Semantics. Read as an iff by default: the method throws a matching
exception exactly when some arm's condition holds — each arm is a
must-throw (condition on entry ⇒ the method throws, not returns),
and the arm-set as a whole claims only-when (a matching throw ⇒
some arm's condition held). exhaustive = false disclaims the only-when
half for the whole arm-set (a one-directional, JML signals-style arm):
the condition is sufficient for the throw but the set does not claim to
list every reason — useful when the full condition is inexpressible (for
example, Integer.parseInt: s == null is a true sufficient
condition, while "malformed or out of range" is not reasonably a parameter
closure). Note what neither mode claims: exhaustiveness is (at most) over the
conditions for the exception types mentioned, never over exception
types themselves — declaring an ArithmeticException arm says nothing
about whether the method can throw anything else. And no claim, however
exhaustive, reasons about VM resource conditions: an OutOfMemoryError
or StackOverflowError is outside contract semantics — the checked
wrapper passes any VirtualMachineError through unjudged, whatever the
arm types.
Checking. Weaving implements the contract; checked = true
additionally verifies it at runtime, in the same assertion style as
Ensures: on a normal return, no non-woven arm's condition may have held
(must-throw), and an escaping exception matching some arm's type must be
justified by a matching arm's condition having held on entry (only-when —
checked only for exhaustive arm-sets). A broken implementation raises
ThrowsIfViolation — never the declared
exception, which is defined behaviour delivered at entry; a justified throw
always propagates untouched. checked is set-level: if any arm
is checked, the whole arm-set is checked — every non-woven arm is
must-throw checked, and every arm serves as an only-when justifier (a sibling
cannot silently opt out). A pleasant corollary:
woven = false, direct = false plus checked = true
runtime-validates a claim about invoked third-party code — a wrong
specification is exposed, not silently believed.
The annotation is runtime-retained and repeatable, so the exceptional contract
is structured metadata available to documentation generators, static analysers,
verification tools, and AI coding agents — none of which can reliably consume
javadoc prose or discover guarded throw sites by body traversal. Only the
must-throw guard of a woven arm and the checked wrapper affect
generated code; exception, exhaustive and direct are
otherwise consumed by tools.
| Type | Name and Description |
|---|---|
Class |
valueReturns the closure class that evaluates the throw condition, a boolean expression over the method's parameters. |
| Type | Name and Description |
|---|---|
boolean |
checkedtrue: verify the contract at runtime in the assertion style of
Ensures — a normal return with a non-woven arm's condition held, or
an unjustified escaping throw of a matching type (exhaustive
arm-sets only), raises a
ThrowsIfViolation. |
boolean |
directInformation for readers and tools, with no effect on bytecode; ignored (implicitly true) for woven arms. |
Class<? extends Throwable> |
exceptionThe exception type thrown when the condition holds. |
boolean |
exhaustiveNo effect on generated code by itself. |
boolean |
woventrue (default): generate the guard-throw at method entry.
|
true: verify the contract at runtime in the assertion style of
Ensures — a normal return with a non-woven arm's condition held, or
an unjustified escaping throw of a matching type (exhaustive
arm-sets only), raises a
ThrowsIfViolation. false
(default): no runtime checking beyond any WOVEN guard.
Information for readers and tools, with no effect on bytecode; ignored
(implicitly true) for woven arms. true (default): a
hand-written throw statement lives in this body. false:
the exception arises from code this method executes — a call, possibly
transitively, or a runtime operation — so there is no throw statement in
this body to find. Most users never set this; a verification or analysis
tool unable to find the promised throw in the body is the usual prompt.
The exception type thrown when the condition holds. A woven arm generates
new <exception>(String), so the type should provide a
(String) constructor (all the standard JDK exceptions do).
No effect on generated code by itself. Under checked = true it
gates the escaping-throw check: true (default) claims the listed
conditions are the only reasons a matching exception is thrown,
so an escaping match with no condition held is a
ThrowsIfViolation; false
says the method may throw the same exception for other, unlisted reasons
— escaping throws are never judged (and verification tools likewise skip
the only-when direction). One false arm disclaims the check for
the whole arm-set. In specification terms: true is an iff,
false a one-directional (JML signals-style) sufficient
condition — the honest choice when the full condition is unstatable
(Integer.parseInt: s == null is a true sufficient half;
"malformed or out of range" is not a parameter closure).
Returns the closure class that evaluates the throw condition, a boolean expression over the method's parameters.
true (default): generate the guard-throw at method entry.
false: the throw already exists (see direct for where) —
nothing is generated.