Skip to main content

Section 7.1 Introduction to Exceptions

Subsection 7.1.1 When Errors Go Unnoticed

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?
When a user enters an account number and it doesn’t exist, what should your method return?
  • 0 for "not found"? But some users might actually have a $0 balance.
  • -1? What if the bank introduces negative balances?
  • null? That could cause a NullPointerException if we forget to check it.
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.

Subsection 7.1.2 Why Returning an Error Code Fails

Let’s look at an example using an iterator that returns a sentinel value. Here is the method contract before we see the code:
  • If there are numbers left: return the next one.
  • If no numbers remain: return -999 (caller must check!).
  • No compiler enforcement: since -999 is just another integer, Java won’t warn us if we misuse it.
Now the actual code:
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:
The compiler never warned us that -999 was used in the average. The program silently produces a meaningless result.

Subsection 7.1.3 Using 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.
Method Contract (Exception Approach):
  • If data exists: return the next number.
  • If no data remains: throw NoSuchElementException.
  • The caller cannot ignore this mistake: Java forces them to handle it or the program crashes.
Here’s how we can rewrite the iterator:
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.

Subsection 7.1.4 Handling Exceptions Gracefully

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.
General Pattern:
try {
    // Code that might throw an exception
} catch (ExceptionType e) {
    // Handle the error (don't ignore it!)
}
For example, safely handling NoSuchElementException in our iterator loop:
This code catches the exception and prints a message instead of crashing or using invalid data.

Subsection 7.1.5 How Java Handles Exceptions

Before running the code below, predict: Will Java stop immediately, or will it keep looking for a handler? Why?
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.

Subsection 7.1.6 Conclusion & Next Steps

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 should you use exceptions?
  • When ignoring a failure would cause bigger problems (for instance, data corruption)
  • When there’s no sensible "default" value to return (like -999) and you need to force the caller’s attention
  • 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*.

Exercises 7.1.7 Exercises

1. Multiple-Choice: Sentinel Value vs. Exception.

In Java, why might throwing an exception be preferable to returning a special “sentinel value” (like -999) when something goes wrong?
  • Exceptions force the caller to handle the error or let the program crash, preventing silent misuse of an invalid return value.
  • Correct. With sentinel values, the compiler won’t warn you if you ignore them. Exceptions make errors harder to ignore.
  • Returning special integer codes is no longer allowed in modern Java versions, so exceptions are the only option.
  • No. Java still allows any integer return. Exceptions are just a safer design for error handling.
  • Exceptions automatically correct the error and let the program continue with valid data.
  • No. Exceptions don’t fix errors; they indicate a problem that the caller must address or handle.
  • Using a sentinel value can never crash a program, but using exceptions always does.
  • No. A sentinel value can lead to incorrect logic if not handled. Exceptions can be caught, preventing crashes.

2. Multiple-Choice: Sentinel Misuse.

Consider this snippet using a sentinel-based iterator:
int val = it.next(); // could return -999 if no elements // ... int sum = val + 10; // no check for sentinel
What is the main risk if we don’t check for the sentinel value -999?
  • The program will always crash with a NullPointerException.
  • No. -999 is an int, so it won’t throw NullPointerException.
  • The code becomes slower because it’s performing extra checks on the sentinel.
  • No. The bigger risk is logical errors, not just performance concerns.
  • The code may silently use -999 as if it were a valid value, producing an incorrect or misleading result.
  • Exactly. That leads to “invisible” errors that can propagate widely.
  • Java will throw an exception if you forget to handle -999 properly.
  • No. Sentinel misuse is silent. That’s why returning a sentinel can lead to subtle bugs.

3. Multiple-Choice: Throwing Exceptions.

You have this code in a method:
throw new NoSuchElementException("Not found");
Which statement best describes what happens immediately after the throw statement executes?
  • 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.

4. Multiple-Choice: try/catch Basics.

Which statement about try/catch in Java is correct?
  • 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.

5. Multiple-Choice: Exception Propagation.

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?
  • The exception propagates back up to main. If main doesn’t catch it either, the program terminates and prints a stack trace.
  • Yes. Java unwinds the call stack, looking for a handler. If none is found, it ends with a stack trace.
  • It restarts methodB from the beginning, ignoring the exception details.
  • No. Java does not “retry” methods after an exception. It either handles or propagates the error.
  • It silently sets the return value of methodB to null, letting execution continue in main as normal.
  • No. Uncaught exceptions don’t degrade to null; they propagate and can terminate the program if unhandled.
  • The JVM forcibly calls System.exit(0), indicating a clean exit with no error message.
  • No. The default behavior is to print a stack trace and exit with a non-zero code, not a silent exit.

6. Short-Answer: Checked vs. Unchecked Exceptions.

Briefly explain the main difference between checked and unchecked exceptions in Java. When does the compiler force you to handle an exception?
Answer.
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.

7. Short-Answer: try/catch Rationale.

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.
Answer.
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.
You have attempted of activities on this page.