Unit 08 · lesson
Exceptions: Reject Invalid Requests Without Hiding the Failure
Interfaces define what callers are allowed to ask an object to do. The implementation still has to decide what happens when a request violates the component's rules.
You have already seen IllegalArgumentException in constructor and method validation. This lesson goes deeper into what an exception means, how throw and try/catch work, and why catching an exception does not automatically mean the underlying problem was recovered.
Start with an invalid update
Suppose a range source models a sensor with a declared interval of 0.10 to 2.00 meters.
class RangeSensor implements RangeSource {
private final String id;
private final double minRangeMeters;
private final double maxRangeMeters;
private double rangeMeters;
RangeSensor(
String id,
double minRangeMeters,
double maxRangeMeters,
double initialRangeMeters
) {
this.id = id;
this.minRangeMeters = minRangeMeters;
this.maxRangeMeters = maxRangeMeters;
updateRange(initialRangeMeters);
}
public void updateRange(double newRangeMeters) {
if (newRangeMeters < minRangeMeters
|| newRangeMeters > maxRangeMeters) {
throw new IllegalArgumentException(
"Range outside declared interval: "
+ newRangeMeters
);
}
rangeMeters = newRangeMeters;
}
@Override
public String getId() {
return id;
}
@Override
public double getRangeMeters() {
return rangeMeters;
}
}
This call is accepted:
sensor.updateRange(0.42);
This call is rejected:
sensor.updateRange(3.70);
The method does not quietly clamp 3.70 down to 2.00, store it anyway, or pretend nothing happened. It throws an exception because the request violates the rule written into the class.
What throw does
This statement:
throw new IllegalArgumentException(
"Range outside declared interval: " + newRangeMeters
);
creates an exception object and immediately stops normal execution of the current method.
Code after the throw in that path does not run.
That is why the assignment must come after validation:
if (invalid) {
throw new IllegalArgumentException(...);
}
rangeMeters = newRangeMeters;
If the value is invalid, the field never changes.
Preserve the last valid state
Suppose the object currently stores:
0.42 m
Then:
sensor.updateRange(3.70);
throws before assignment.
After the failed request, the object still stores:
0.42 m
That behavior is useful. The invalid request is rejected without corrupting the previously valid state.
A badly ordered implementation would do this:
public void updateRange(double newRangeMeters) {
rangeMeters = newRangeMeters;
if (newRangeMeters < minRangeMeters
|| newRangeMeters > maxRangeMeters) {
throw new IllegalArgumentException("invalid range");
}
}
Now 3.70 is stored before the method throws.
The caller receives an exception, but the object has already entered the invalid state.
This is a subtle but important bug: detection happened too late.
Catch an exception when you can respond meaningfully
An exception can travel up to the code that called the method.
The caller may choose to catch it:
try {
sensor.updateRange(3.70);
} catch (IllegalArgumentException error) {
System.out.println(
"Rejected update for " + sensor.getId()
+ ": " + error.getMessage()
);
}
Possible output:
Rejected update for range-front: Range outside declared interval: 3.7
The try block contains code that may throw.
The catch block handles the named exception type if it occurs.
The variable error refers to the exception object, which contains information such as the message.
Catching is not the same as fixing
This is one of the most important ideas in the lesson.
After the catch block prints:
Rejected update for range-front...
the program knows that an invalid request occurred.
It has not proven why the bad value appeared.
Possible causes could include:
- a test intentionally supplied an out-of-range value;
- a conversion from centimeters to meters is wrong;
- a sensor produced a bad reading;
- configuration limits are wrong;
- another component passed the wrong data.
The exception tells you where a rule was violated. Root-cause diagnosis may require more evidence.
The empty catch block is dangerous
Never use this pattern as a default:
try {
sensor.updateRange(3.70);
} catch (IllegalArgumentException error) {
}
The exception disappears.
The program may continue, but the record of the rejected request is lost.
That can make a robot application look healthy while important failures are being silently ignored.
If an exception is intentionally ignored, there should be a very clear reason. For this course, preserve the context instead.
Catch the exception you expect
Another weak pattern is:
catch (Exception error) {
System.out.println("something failed");
}
Exception is broad. It can hide the distinction between different problems.
When you expect one specific invalid-input rule, catching the specific type is clearer:
catch (IllegalArgumentException error)
The code communicates which failure it is prepared to handle.
Error messages need context
Compare:
invalid
with:
Rejected update for range-front: 3.70 m outside 0.10..2.00 m
The second record is much more useful during diagnosis.
A strong message often includes:
- which component;
- which value;
- which rule was violated;
- the relevant unit.
For example:
throw new IllegalArgumentException(
"Sensor " + id
+ " rejected " + newRangeMeters + " m"
+ "; allowed interval is "
+ minRangeMeters + ".." + maxRangeMeters + " m"
);
Do not include secrets, credentials, or private data in diagnostic messages. Robot component IDs and controlled numeric values are appropriate here.
Interface contracts and failure behavior
Recall:
interface RangeSource {
String getId();
double getRangeMeters();
}
The interface does not currently expose updateRange(...).
That is intentional. RangeSource describes what a consumer of range data can read.
The concrete RangeSensor implementation may have additional update behavior and validation that its data-provider side uses.
This helps keep the public contract focused.
An interface does not need to expose every internal operation of every implementation.
Worked case: preserve the failure and continue safely
Suppose you receive a controlled sequence:
double[] updates = {0.42, 0.38, 3.70, 0.35};
You want to attempt each update without losing the invalid one.
for (double value : updates) {
try {
sensor.updateRange(value);
System.out.println(
"accepted " + value + " m"
);
} catch (IllegalArgumentException error) {
System.out.println(
"rejected " + value + " m"
+ " reason=" + error.getMessage()
);
}
}
Possible output:
accepted 0.42 m
accepted 0.38 m
rejected 3.7 m reason=Range outside declared interval: 3.7
accepted 0.35 m
After the rejected 3.70, the object keeps the last accepted reading until the 0.35 update succeeds.
This is a controlled recovery policy: record the invalid request and continue processing later values.
A different robot system might choose to stop instead. The correct policy depends on the requirements.
Do not turn exceptions into normal control flow
Exceptions are useful for exceptional or invalid conditions. They should not replace ordinary if statements for every normal branch.
For example, if a method frequently needs to answer whether a value is inside a range, a boolean method may be clearer:
boolean isValidRange(double value) {
return value >= minRangeMeters
&& value <= maxRangeMeters;
}
Use normal conditions for normal decisions. Use exceptions when a caller violates a required contract and normal execution should not continue along that path.
Practice: repair a broken update method
This method has two problems:
void updateBatteryPercent(double percent) {
currentPercent = percent;
if (percent < 0 || percent > 100) {
throw new IllegalArgumentException("bad");
}
}
Rewrite it so that:
- invalid values are rejected before assignment;
- the message includes the bad value and the allowed range;
- the last valid state is preserved;
- a caller can catch the specific exception and print the component ID plus the reason.
Then explain the difference between these statements:
The invalid request was detected.
and:
The cause of the invalid request was diagnosed.
In the next lesson, the word interface changes context. You will compare a Java interface, which is a programming-language contract, with ROS 2 interfaces, which define communication data exchanged by nodes.