Handling Exceptions
Introduction
This lesson covers the fourth objective of the Oracle Certified Professional Java SE 21 Developer exam (1Z0-830): Handling Exceptions. Oracle does not publish a weighting per objective, but exception handling is a rich source of "does it compile?" and "what is printed?" questions because the rules are precise: which exceptions must be declared, which catch orders are legal, what finally does to a returned value, and how try-with-resources closes and suppresses.
You will learn the exception hierarchy, the full try/catch/finally semantics, multi-catch, try-with-resources with suppressed exceptions, the overriding rules for throws, and how to design custom exceptions. Every section ends with the traps the exam uses.
Learning objectives
After this lesson you will be able to:
- Classify any exception as checked, unchecked or an error, and say what the compiler demands for each.
- Trace
try/catch/finallyflow, includingreturninsidefinallyand exceptions thrown fromcatchblocks. - Write legal multi-catch clauses and order catch blocks without compile errors.
- Use try-with-resources, predict the closing order and read suppressed exceptions.
- Apply the
throwsrules when overriding methods and implementing interfaces. - Create custom checked and unchecked exceptions, with chaining of the cause.
- Choose between
throw,throws, rethrowing and wrapping in realistic code.
1. The exception hierarchy
All throwable types descend from java.lang.Throwable. It has two branches:
Error: serious problems the application is not expected to handle (OutOfMemoryError,StackOverflowError,AssertionError). Unchecked.Exception: conditions an application may handle. Under it,RuntimeExceptionand its subclasses are unchecked; every otherExceptionsubclass is checked.
| Category | Examples | Compiler requires handle or declare? |
|---|---|---|
| Checked | IOException, SQLException, InterruptedException, ClassNotFoundException, Exception itself |
Yes |
| Unchecked (runtime) | NullPointerException, IllegalArgumentException, ArithmeticException, ClassCastException, ArrayIndexOutOfBoundsException, NumberFormatException, IllegalStateException, UnsupportedOperationException |
No |
| Error | OutOfMemoryError, StackOverflowError, ExceptionInInitializerError |
No |
"Handle or declare" means: either catch the checked exception in a try/catch, or list it in the throws clause of the method.
static String readFirstLine(Path p) throws IOException { // declared
return Files.readAllLines(p).get(0); // may throw IOException
}
Remember some specifics: NumberFormatException extends IllegalArgumentException; ArrayIndexOutOfBoundsException and StringIndexOutOfBoundsException extend IndexOutOfBoundsException; FileNotFoundException extends IOException; UncheckedIOException wraps an IOException as a runtime exception.
2. try, catch and finally
try {
int qty = Integer.parseInt(input); // may throw NumberFormatException
process(qty);
} catch (NumberFormatException e) {
log.warn("bad quantity: " + input);
} catch (IllegalStateException e) {
...
} finally {
metrics.increment("order.parsed"); // always runs
}
Rules:
- A
tryneeds at least onecatchor afinally(or, with resources, neither). - Catch blocks are tested top to bottom; the first compatible one runs. A catch for a subclass after a catch for its superclass is a compile error ("exception has already been caught"). Put the most specific first.
- Catching a checked exception that the
tryblock cannot throw is a compile error (catch (IOException e)around code that never throws it). This does not apply toException,Throwableor unchecked types. finallyruns after the try and catch blocks whether an exception occurred or not. It does not run only if the JVM exits (System.exit) or the thread is killed or the JVM crashes.- An exception thrown from a
catchblock propagates to the caller afterfinallyruns.
return and finally
A return inside try evaluates its expression first, then runs finally. If finally itself returns or throws, it replaces the earlier result or exception.
static int f() {
int x = 1;
try {
return x; // value 1 is saved
} finally {
x = 2; // changes the variable, not the saved value
}
} // returns 1
static int g() {
try {
throw new RuntimeException("lost");
} finally {
return 5; // swallows the exception, returns 5
}
}
For a mutable returned object, changes made in finally are visible because the reference is the same; for primitives and immutable values they are not.
3. Multi-catch and rethrow
A multi-catch lists alternatives separated by |:
try {
run();
} catch (IOException | SQLException e) {
log.error("infrastructure failure", e);
throw e; // precise rethrow, e is effectively final
}
Rules:
- The alternatives must not be related by subclassing:
catch (FileNotFoundException | IOException e)is a compile error because one is a subtype of the other. - The multi-catch parameter is implicitly
final: you cannot assign toe. - Its static type is the closest common supertype, so you can call only methods of that supertype.
- Precise rethrow: when you rethrow an effectively final catch parameter, the compiler knows the real exceptions the
trycan throw. A method catchingExceptione and rethrowing it only needs to declare the checked exceptions thrown inside thetry.
4. try-with-resources
Any object implementing AutoCloseable (or Closeable) can be declared in the resource header. It is closed automatically, in reverse order of declaration, before any catch or finally runs.
try (var in = Files.newBufferedReader(src);
var out = Files.newBufferedWriter(dst)) {
String line;
while ((line = in.readLine()) != null) {
out.write(line);
out.newLine();
}
} catch (IOException e) {
log.error("copy failed", e);
} finally {
log.info("copy attempted");
}
Details the exam checks:
- Resources are implicitly
final. Since Java 9 you may use an existing variable that isfinalor effectively final:try (reader) { ... }. close()ofAutoCloseabledeclaresthrows Exception, so if the resource type isAutoCloseableyou must handleException;Closeable.close()declaresIOException; many classes narrow it further.- If the body throws an exception and
close()also throws, the body exception is the primary one and thecloseexception is attached as suppressed; read it withe.getSuppressed(). - If a resource constructor throws, resources already opened are closed, and the one that failed is not.
- The order of output for a closing trace is: body, resources closed in reverse, then
catch, thenfinally.
class Res implements AutoCloseable {
final String name;
Res(String name) { this.name = name; System.out.print("open " + name + "; "); }
public void close() { System.out.print("close " + name + "; "); }
}
try (Res a = new Res("A"); Res b = new Res("B")) {
System.out.print("body; ");
} finally {
System.out.print("finally");
}
// open A; open B; body; close B; close A; finally
5. Checked exceptions, throw and throws
throw is a statement that raises one exception object; throws is a clause in the method signature that lists exceptions the method may propagate.
- You can throw a checked exception only if you catch it or declare it.
- Unreachable code after
throwis a compile error. throw null;compiles and produces aNullPointerExceptionat run time.- A method may declare unchecked exceptions in
throws, but callers need not handle them.
Overriding and throws
An overriding method can declare fewer, narrower or no checked exceptions, but not new or broader checked exceptions. Unchecked exceptions are unrestricted.
class Repo { void save() throws IOException { } }
class FileRepo extends Repo {
@Override void save() throws FileNotFoundException { } // narrower: OK
}
class BadRepo extends Repo {
@Override void save() throws Exception { } // broader: compile error
}
The same rule applies when implementing an interface method. Constructors do not override, but an implicit super() call must be compatible with what the subclass constructor declares: if the parent constructor throws a checked exception, the child constructor must declare it (or a supertype).
6. Custom exceptions and chaining
Extend Exception for a recoverable condition the caller should be forced to consider, RuntimeException for programming errors or conditions the caller cannot reasonably recover from. Provide constructors for a message and for a cause, so stack traces keep the root problem.
public class EnrollmentException extends Exception {
private final String courseId;
public EnrollmentException(String courseId, String message, Throwable cause) {
super(message, cause);
this.courseId = courseId;
}
public String courseId() { return courseId; }
}
void enroll(String user, String course) throws EnrollmentException {
try {
repo.insert(user, course);
} catch (SQLException e) {
throw new EnrollmentException(course, "cannot enroll " + user, e); // wrap, keep cause
}
}
getCause() returns the wrapped exception, printStackTrace() shows "Caused by:" sections, and initCause can set it later. Do not swallow exceptions with an empty catch; at least log them or rethrow.
assert is related but different: assert cond : message; throws AssertionError only if assertions are enabled with -ea; they are disabled by default, so assertions must never carry side effects needed by the program.
7. Frequent runtime exceptions and their causes
| Exception | Typical cause |
|---|---|
NullPointerException |
Dereferencing null, unboxing a null Integer |
ArrayIndexOutOfBoundsException |
Index < 0 or >= length |
ClassCastException |
Invalid downcast |
ArithmeticException |
Integer division by zero (floating point gives Infinity/NaN) |
NumberFormatException |
Integer.parseInt("12a") |
IllegalArgumentException |
Argument rejected by a method |
IllegalStateException |
Method called at the wrong time |
ConcurrentModificationException |
Structural change of a list while iterating |
UnsupportedOperationException |
Mutating List.of(...) |
ArrayStoreException |
Storing a wrong type in a covariant array |
ExceptionInInitializerError |
An exception escaping a static initializer |
StackOverflowError |
Unbounded recursion |
8. Designing exception boundaries
Good exception handling is as much design as syntax. Three habits make code easier to maintain and also help you reason about exam snippets.
Catch where you can act. A low-level method that cannot recover should let the exception propagate (or wrap it) rather than log and continue. A method near the user interface, such as a web controller, is the right place to turn an exception into an HTTP 400 or 500 response and a log entry.
Do not use exceptions for normal control flow. Parsing a number with try { Integer.parseInt(s) } catch (NumberFormatException e) is acceptable for input validation, but looping until an ArrayIndexOutOfBoundsException occurs is not: it is slow and obscures intent.
Preserve information. When you wrap, always pass the cause. When you rethrow, keep the original object (throw e;) so the stack trace points at the real failure. Creating a new exception with only the message loses the trace. When a cleanup step in finally can itself fail, prefer try-with-resources, which keeps both failures through suppression instead of letting the second hide the first.
Here is a short comparison of the choices you meet on the exam:
| Situation | Best choice | Why |
|---|---|---|
| Caller can recover (retry, ask the user) | Checked custom exception | The compiler forces the caller to decide |
| Caller made a programming mistake (null, bad index) | Unchecked (IllegalArgumentException, NullPointerException) |
Fixing the code is the remedy, not catching |
| Resource must be released | try-with-resources | Closes in reverse order, keeps suppressed errors |
| Several unrelated failures handled identically | Multi-catch | One handler, no duplicated code |
| Lower layer exception should not leak | Wrap with cause | Keeps layers independent, keeps the root cause |
Short example. A payment gateway client throws java.net.http.HttpTimeoutException (a subclass of IOException). The checkout service must show "try again" to the shopper but must never charge twice. Catching IOException at the checkout boundary, logging the order id and returning a retry response is correct; swallowing the exception and returning success would be a business bug that no compiler can detect.
Worked example: order of output
Statement. What is printed?
public class Main {
static String run() {
StringBuilder sb = new StringBuilder("a");
try {
sb.append("b");
Object o = "text";
Integer i = (Integer) o;
sb.append("c");
} catch (ClassCastException e) {
sb.append("d");
return sb.append("e").toString();
} finally {
sb.append("f");
System.out.print(sb + " ");
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(run());
}
}
Step 1. sb starts as a; append("b") gives ab.
Step 2. The cast of a String to Integer throws ClassCastException, so append("c") is skipped.
Step 3. The catch block appends d, then e, producing abde. toString() creates the return value "abde", a separate immutable String that is saved.
Step 4. finally appends f to the builder (now abdef) and prints abdef with a trailing space. It cannot change the saved String.
Step 5. The method returns the saved "abde", and main prints it.
Solution. The output is abdef abde.
Why other answers are worse. abdef abdef assumes the builder is returned instead of an already created String. abde abdef reverses the order of finally and the print in main. abc... forgets that the cast fails before c is appended.
Common pitfalls and exam tips
- Exam tip: first decide compile or not (checked exceptions, catch order, unreachable code), and only then trace the output.
- A
catchof a checked exception that thetrybody cannot throw is a compile error, butcatch (Exception e)is always allowed. - Do not
returnfromfinally: it hides exceptions and values. Expect questions that depend on it. catch (Exception | RuntimeException e)fails: the types are related.- In try-with-resources the resource variable is final; reassigning it in the body is a compile error.
- If both the body and
close()throw, the caller sees the body exception and finds the other ingetSuppressed(). InterruptedExceptionis checked and thrown byThread.sleep; forgetting to handle it is a classic compile error.- A static initializer throwing an unchecked exception produces
ExceptionInInitializerError, and later uses of the class giveNoClassDefFoundError. - Do not catch
ThrowableorErroras routine practice.
Mini case study: importing a course file
An admin import job reads a JSON file, parses lessons and inserts them in a database. Failures must be reported with the lesson id, the file must always be closed, and the job must continue with the next file after a parsing error but stop on a database outage.
Solution. Define LessonParseException extends Exception (recoverable per file) and let SQLException propagate as a wrapped IllegalStateException("database unavailable", e) (not recoverable here). Open the file with try-with-resources so it is closed even on failure. Inside the loop over files, catch only LessonParseException, log it and continue. Because IllegalStateException is unchecked and not caught, it ends the job.
for (Path file : files) {
try (var reader = Files.newBufferedReader(file)) {
importer.load(reader); // throws LessonParseException, SQLException
} catch (LessonParseException e) {
log.warn("skipping " + file + " lesson " + e.lessonId(), e);
} catch (SQLException e) {
throw new IllegalStateException("database unavailable", e);
} catch (IOException e) {
log.warn("cannot read " + file, e);
}
}
Notice that FileNotFoundException would be caught by the IOException clause, which must appear after the more specific ones; the Exception supertype is never used, so unexpected runtime bugs are not hidden. The wrapping keeps the SQL root cause for diagnosis.
Key terms
- Checked exception: an
Exceptionthat is not aRuntimeException; must be handled or declared. - Unchecked exception: a
RuntimeExceptionorError; no compiler requirement. - Multi-catch: a single catch clause handling several unrelated exception types.
- Suppressed exception: an exception from
close()attached to the primary exception. - Precise rethrow: rethrowing an effectively final catch parameter with its actual thrown types.
- Exception chaining: wrapping a cause inside a new exception.
- AutoCloseable: interface whose
close()is called by try-with-resources. - Handle or declare: the compiler rule for checked exceptions.
Summary checklist
- I can classify an exception as checked, unchecked or error from the hierarchy.
- I can order catch blocks correctly and spot "already been caught" errors.
- I can predict what
finallydoes to returned values and exceptions. - I can write multi-catch clauses and know the parameter is final.
- I can trace resource closing order and read suppressed exceptions.
- I can apply the throws rules for overriding and constructors.
- I can design a custom exception that keeps its cause.
Official sources
- Oracle exam page for 1Z0-830 (objectives): https://education.oracle.com/java-se-21-developer-professional/pexam_1Z0-830 (opens in a new tab)
- The Java Tutorials, Exceptions: https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html (opens in a new tab)
- Java Language Specification, Java SE 21, chapter 11 (Exceptions): https://docs.oracle.com/javase/specs/jls/se21/html/jls-11.html (opens in a new tab)
- Java Language Specification, Java SE 21, section 14.20 (The try statement): https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.20 (opens in a new tab)
- API documentation for
AutoCloseable: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/AutoCloseable.html (opens in a new tab)