What happens when an error goes completely unnoticed until it causes panic? Imagine you’re debugging a banking app. A customer reports seeing "-1.00" on their balance sheet and grows alarmed. After hours of investigation, you discover the real issue: a missing account check silently returned -1 as a sentinel value, and the code treated it as real money. How did this happen?
This is a sentinel value problem. If we pick a special return value to mean "error" but forget to handle it, bad things happen. Since Java treats all numbers equally, it can’t warn us when we accidentally use -1 or 0 as if it were valid data.
The fundamental flaw? The compiler doesn’t enforce the check. The program compiles, and everything appears fine—until a silent error snowballs into real-world consequences. Instead of relying on human discipline to notice mistakes, we need a mechanism that forces the caller to acknowledge errors. This is what exceptions provide.
public class SentinelIterator {
private int[] data;
private int currentIndex;
public SentinelIterator(int[] arr) {
data = arr;
currentIndex = 0;
}
public int next() {
if (currentIndex >= data.length) {
return -999; // Sentinel value
}
return data[currentIndex++];
}
}
Consider a caller that forgets to check the sentinel. We’ll demonstrate this with an average calculation, where using -999 makes no sense:
Subsection7.1.3Using Exceptions to Force Error Handling
To avoid sentinel misuse, we can throw an exception when no more elements remain. In Java, throw immediately stops execution and signals a problem, rather than returning a faulty value.
import java.util.NoSuchElementException;
public class ExceptionIterator {
private int[] data;
private int currentIndex;
public ExceptionIterator(int[] arr) {
data = arr;
currentIndex = 0;
}
public int next() {
if (currentIndex >= data.length) {
throw new NoSuchElementException("No more elements");
}
return data[currentIndex++];
}
}
Now, if the caller forgets to check whether more elements remain, the program won’t produce bad data—it will throw an exception and stop.
If we want to recover from an exception instead of crashing, we wrap the risky code in a try block and provide a matching catch. Catching every exception without addressing the problem can hide real issues. Use try/catch only when you need to recover from an error—not just to silence it.
If we run this code, Java looks for a matching catch. Finding none, it keeps unwinding until it reaches the end of main, ultimately terminating the program and printing a stack trace.
main() calls riskyMethod()
└── riskyMethod() throws NoSuchElementException
├── No catch found → propagates up
├── No catch in main() → propagates further
└── Java terminates the program with an error message
If a matching catch block is found anywhere up the stack, Java stops searching and executes that block instead of crashing the program.
By replacing sentinels with exceptions, we ensure errors must be handled. But not all exceptions behave the same—some, like IOException, are checked exceptions, which Java forces you to handle. Others are unchecked and let you decide whether to catch them or fix the underlying bug.
When you need to prevent execution from continuing incorrectly—either by stopping execution or forcing corrective action, whether at compile-time (checked exceptions) or runtime (unchecked exceptions).
Some exceptions, like IOException, must be handled before your code will even compile—while others, like NullPointerException, can happen at any time. Let’s see why Java makes this distinction *in the next section*.
Execution of that method stops; Java looks for a matching catch block higher on the call stack. If none is found, the program terminates with a stack trace.
Exactly. Once you throw an exception, normal flow stops, and Java “unwinds” to find a handler.
The program prints the message "Not found" on the console and continues execution in the same method.
No. Throwing an exception halts normal flow. It doesn’t just print a message.
Java calls System.exit(0) automatically, abruptly shutting down without any trace.
No. Java doesn’t do that. If unhandled, it prints a stack trace and ends with a nonzero status code (not clean exit).
An exception object is created but not thrown to the caller, so the method finishes normally, returning null by default.
No. throw ensures the exception leaves the method unless caught locally.
A try block encloses code that might throw an exception; a matching catch block handles a specific exception type, preventing the program from crashing if that exception occurs.
Correct. That’s the fundamental purpose of try/catch.
You can only have one catch block for each try in Java, and it must handle all exceptions.
No. Java allows multiple catch blocks for different exception types.
try blocks must always be followed by finally; otherwise the code won’t compile.
No. finally is optional; you only need it if you want a block that always executes.
Once an exception is caught, the program restarts from the try block.
No. After catching an exception, execution continues after the catch block, not from the beginning of try.
Consider this sequence of calls: main() --> methodA() --> methodB(). If methodB throws an unchecked exception and there’s no matching catch block in methodB or methodA, what happens next?
Checked exceptions (e.g., IOException) must be handled or declared in the method signature at compile time, or your code won’t compile. Unchecked exceptions (e.g., NullPointerException) don’t require explicit handling; they can occur at runtime, and the compiler doesn’t force you to catch or declare them.
In your own words, why might we surround code with try blocks and catch exceptions, instead of letting them propagate? Give one practical scenario where try/catch makes sense.
Using try/catch allows us to gracefully recover or handle an expected error rather than crashing. For example, reading from a file: if the file is missing or corrupted, we can catch IOException and alert the user or use a default file instead of just terminating.