12.2
Checked, Unchecked, and Exceptions of Your Own
Two things can fail the same way and the compiler treats them completely differently. The rule is real, the reasoning behind it is contested, and every language designed after Java went the other way.
Previously on
Section 12.1 gave you throw, try, catch, finally and the stack trace, plus the fact that Error and Exception are siblings.
It also left two lines side by side:
Files.readString(path); // will not compile without a try or a throws
Integer.parseInt("abc"); // compiles fine, throws at run timeThe problem
Both of those methods can fail, and the compiler treats them nothing alike.
Write the first one and you cannot get past the build:
String text = Files.readString(path);
// error: unreported exception java.io.IOException; must be caught or declared to be thrownWrite the second and nothing happens until it runs:
int port = Integer.parseInt(config.get("port")); // compiles. Throws on "abc".Nothing about the two failures is different in kind. A file might not be there. A string might not be a number. Both are ordinary things that go wrong with input you did not control. One of them the compiler insists you handle, and about the other it says nothing at all.
The rule that produces this is unrelated to how likely or serious the failure is. It is a question of which class the exception extends, and that is it.
Then there is the problem of your own failures. You are writing a payment system, and a card is declined:
throw new Exception("card declined");That works, and it gives the caller nothing. They cannot catch declined cards separately from network failures, because both arrive as Exception. They cannot get the decline code, because a message is a string. To respond differently to different failures, they would have to read the message text, which breaks the first time someone rewords it.
The idea
The whole hierarchy is four levels, and one of them decides everything.
Throwable
├── Error unchecked. Not yours to catch.
└── Exception CHECKED. The compiler forces a catch or a throws.
└── RuntimeException UNCHECKED. The compiler says nothing.RuntimeException extends Exception, and that single inheritance is the entire rule:
Exception.class.isAssignableFrom(RuntimeException.class) // true
Exception.class.isAssignableFrom(Error.class) // falseSo: extends RuntimeException, unchecked. Extends Exception but not RuntimeException, checked. Nothing else is involved.
| Checked | Unchecked | |
|---|---|---|
| Extends | Exception, not RuntimeException | RuntimeException |
| Compiler | forces a catch or a throws | says nothing |
| Examples | IOException, SQLException, ClassNotFoundException | NullPointerException, IllegalArgumentException, NumberFormatException |
| Meant for | things a caller could recover from | bugs in the code |
throw and throws are unrelated, and one letter apart:
void readConfig() throws IOException { // throws: a warning in the signature
if (path == null) {
throw new IllegalStateException("no path"); // throw: it happens here, now
}
}throw is a statement that takes an object. throws is part of the signature and takes types. A method can declare throws and never throw, and it can throw unchecked exceptions with no throws anywhere.
Your own exception is a class that extends one of them.
public class PaymentDeclinedException extends RuntimeException {
private final String declineCode;
public PaymentDeclinedException(String message, String declineCode, Throwable cause) {
super(message, cause);
this.declineCode = declineCode;
}
public String declineCode() { return declineCode; }
}Three things earn their place here.
A distinct type, so callers can catch this and not other failures. A field, so the decline code arrives as data instead of buried in a sentence. A cause, so the original failure is not lost.
Passing the cause is the part people skip:
try {
int port = Integer.parseInt(raw);
} catch (NumberFormatException e) {
throw new ConfigException("bad value for port: " + raw, e); // <- the e
}Now the trace shows your message, then Caused by: and the original underneath. Leave e out and the original trace is gone, and yours starts at the catch block rather than the failure.
Catching several the same way:
try {
process(input);
} catch (NumberFormatException | NullPointerException e) {
log.warn("bad input: {}", e.getMessage());
}One block, two types, no duplication. The catch order still matters when types are related: a parent before a child will not compile, because the child could never be reached.
Under the hood
Going deeperThe stack trace is most of what an exception costs. Two hundred thousand throws, caught immediately, measured:
| What was thrown | 200,000 throws | |
|---|---|---|
| new RuntimeException("x") | trace filled in | 84 ms |
| trace turned off | super(m, null, false, false) | 9 ms |
Nine times faster with the trace turned off. The throwing, unwinding and catching are cheap. Walking the stack to record every frame is not.
Throwable has a four argument constructor for exactly this:
super(message, cause, enableSuppression, writableStackTrace);Pass false for the last one and no trace is captured. This is a real technique for exceptions thrown thousands of times a second on a hot path. It is a terrible default everywhere else, because you have removed the thing that tells you where the problem was.
A surprise worth knowing. What does this throw?
Integer.parseInt(null);Not NullPointerException. It throws NumberFormatException: Cannot parse null string: null. parseInt checks for null itself and reports it as a parsing problem, which is defensible and catches people out. If you wrote catch (NullPointerException e) expecting to handle it, you do not.
Checked exceptions do not survive contact with lambdas.
list.forEach(path -> Files.readString(path)); // will not compileConsumer.accept does not declare throws IOException, and it cannot: it was designed before your method existed, and adding it would break every other use. So a checked exception cannot escape a lambda, and the usual workaround is to catch it inside and wrap it in an unchecked one.
Every stream you wrote in Phase XI has this property. It is one of the strongest practical arguments against checked exceptions, and it arrived a decade after they were designed.
The argument against them, stated plainly. The intent was reasonable: make callers deal with recoverable failures. What happens in practice is this:
try {
doSomething();
} catch (IOException e) {
// TODO
}The compiler asked a question, the developer made it go away, and the failure is now invisible. Forcing an answer does not produce a good one.
The second problem is that throws spreads. A method deep in your code throws IOException, so its caller must declare it, and so must theirs, all the way up. Change the bottom method and every signature above it changes. The alternative is throws Exception on everything, which says nothing.
C#, Kotlin, Scala and Go all looked at checked exceptions and left them out. Java cannot remove them now, and the practice that has settled is to catch checked exceptions early and rethrow them as unchecked ones with the cause attached.
What it costs
Custom exception types multiply. A class per failure gives callers precision and gives your project fifty small files that mostly differ by name. Somewhere between one type for everything and one per failure is a line, and nobody can tell you where it is.
Wrapping has a cost too. Catching, wrapping and rethrowing at each layer produces traces with four or five Caused by sections, and the useful line is at the bottom. Each wrap felt reasonable on its own.
Turning off the stack trace saves nine tenths of the cost and removes the thing that makes an exception useful. It is worth doing on a genuinely hot path and nowhere else, and a future reader will not know why the traces are empty unless you say so.
There is a subtler cost in exception types leaking through your layers. A method declaring throws SQLException has told every caller that it talks to a database. Change to a different store and every signature above it changes with you. Wrapping it in something of your own is what keeps that from happening, which is one more reason to reach for a custom type.
And the empty catch remains the most expensive thing in this section. It costs nothing to write, passes every build, and turns a clear failure into a bug that shows up somewhere else entirely.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`Files.readString(path)` will not compile without a `try`. `Integer.parseInt("abc")` compiles fine. Both can fail. Why the difference?
Show the answer
One is checked and the other is not, and the dividing line is a single class.
Anything extending
RuntimeExceptionis unchecked. The compiler says nothing about it.NumberFormatException,NullPointerExceptionandIllegalArgumentExceptionall live there.Anything extending
Exceptionbut notRuntimeExceptionis checked. The compiler forces every caller to either catch it or declare it.IOExceptionis the one you will meet most.The intended split was: checked means a caller could reasonably recover, unchecked means a bug. The split does not hold in practice, which is the argument in this section.
What is the difference between `throw` and `throws`?
Show the answer
Different words doing unrelated jobs, and the missing letter is the only clue.
throwis a statement. It happens, right now, on that line:throw new IllegalArgumentException("negative").throwsis part of a signature. It is a warning to callers that this method might throw something, and it takes types rather than an object:void read() throws IOException.One does the throwing. The other declares that throwing is possible. A method can have
throwsand never throw, and it can throw an unchecked exception without anythrowsat all.You catch a `NumberFormatException` and throw your own `ConfigException`. What must you pass along, and what happens if you do not?
Show the answer
The original exception, as the cause:
throw new ConfigException("bad value for port", e).Do that and the stack trace shows your exception, then
Caused by:and the whole original trace underneath. The reader gets your context and the line that actually failed.Leave it out and the original trace is gone. Your exception's trace starts at your catch block, so it points at the place you noticed the problem rather than the place it happened. On a deep stack that can be a completely different file.
Every exception has a constructor taking a cause. Passing it costs one word and it is the difference between a report and a dead end.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises100 pointsabout 105 minutes
Find the Dividing Line
ex-12-2-aSort them first, from what you already know, then check.
The rule is one inheritance and nothing else. Not how serious the failure is, not how likely, not whether it is your fault. Anything under RuntimeException is unchecked, and everything else under Exception is checked.
Ask the type system rather than compiling and seeing what breaks. Getting the answer from isAssignableFrom means you understood the rule, and getting it from a red squiggle means the compiler understood it for you.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Divide {
static boolean isUnchecked(Class<?> type)
static boolean isChecked(Class<?> type)
static boolean isNeither(Class<?> type)
static String classify(Class<?> type)
static String catchIt(String path)
static String declareIt(String path) throws IOException
static String callerOfDeclared(String path)
}Answer by asking the type system, not by listing names: the checks pass classes the starter never mentions. classify returns "checked", "unchecked", or "neither, it is an Error". catchIt handles an IOException itself, declareIt passes it up, and callerOfDeclared handles it one level higher.
What your program must do
- Sort all ten into checked and unchecked before writing code
- Answer by asking the type system rather than by trial and error
- Write a line that fails to compile because of a checked exception, and fix it two ways
- Say which of the ten is neither, and why catch of Exception cannot see it
import java.io.*;
import java.sql.SQLException;
public class Divide {
// Sort these before writing any code:
// IOException NumberFormatException SQLException IllegalArgumentException
// FileNotFoundException NullPointerException InterruptedException
// ClassNotFoundException ArithmeticException OutOfMemoryError
//
// Then answer by ASKING THE TYPE SYSTEM. The checks pass classes not listed here.
static boolean isUnchecked(Class<?> type) { return false; } // TODO
static boolean isChecked(Class<?> type) { return false; } // TODO
static boolean isNeither(Class<?> type) { return false; } // TODO
static String classify(Class<?> type) { return ""; } // TODO
// The two ways past a checked exception.
static String catchIt(String path) { return ""; } // TODO: handle it here
static String declareIt(String path) throws IOException { return ""; } // TODO: pass it up
static String callerOfDeclared(String path) { return ""; } // TODO: handle it one level up
public static void main(String[] args) {
// TODO: classify all ten and check against your sorting
}
}
Hint 1
RuntimeException.class.isAssignableFrom(X.class) answers it. That single inheritance is the whole rule, and nothing else is involved.Hint 2
OutOfMemoryError is the odd one. It is not an Exception at all, so neither answer fits and a catch of Exception will never see it.Hint 3almost the answer
throws to your own signature. Catch it when you can do something here. Declare it when the caller is better placed to decide.Lose the Cause, Lose the Trace
ex-12-2-bWrite the bad version first and read what you get.
Your exception has a message, so it looks informative. Then check where its trace begins: at your own catch block, which is the place the problem was noticed. The line that actually failed is not in there at all.
Add the cause and the whole original comes back, underneath yours. One word, and it turns a dead end into a report.
The last part is a small trap with a real lesson. Predict what parseInt(null) throws before you run it, and if you were wrong, that is a catch block you would have written for nothing.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Causes {
static int readPortLosingCause(String raw)
static int readPortKeepingCause(String raw)
static String causeTypeOf(Runnable work)
static String deepestFrameOf(Runnable work)
static String parseNullType()
}ConfigException is a nested unchecked exception with two constructors, one taking a cause. deepestFrameOf follows getCause to the bottom and names where that trace starts: "Integer.parseInt" if it reaches the parse, otherwise the method in this class.
What your program must do
- Throw your exception without the cause and read the whole trace
- Do it again with the cause and describe what appeared
- Say which line the first trace starts at, and why that is unhelpful
- Predict what Integer.parseInt(null) throws, then check
public class Causes {
static class ConfigException extends RuntimeException {
ConfigException(String message) { super(message); }
ConfigException(String message, Throwable cause) { super(message, cause); }
}
// The same job twice. One throws away the original, one keeps it.
static int readPortLosingCause(String raw) { return 0; } // TODO
static int readPortKeepingCause(String raw) { return 0; } // TODO
// "none" when there is no cause, otherwise the cause's simple name.
static String causeTypeOf(Runnable work) { return ""; } // TODO
// Follow getCause to the bottom, and name where THAT trace starts.
static String deepestFrameOf(Runnable work) { return ""; } // TODO
// Predict this one before running it.
static String parseNullType() { return ""; } // TODO: Integer.parseInt(null)
public static void main(String[] args) {
// TODO: throw both versions and read the FULL trace of each
}
}
Hint 1
Hint 2
Caused by: and the original trace underneath. One word, and it turns a dead end into a report.Hint 3almost the answer
Integer.parseInt(null) throws NumberFormatException, not NullPointerException. It checks for null itself and reports it as a parsing problem, so a catch written for NPE never runs.An Exception Worth Catching
ex-12-2-cThree things make a custom exception worth the file it lives in.
A distinct type, so a caller can catch declined cards without also catching network failures. A field, so the decline code arrives as data. A cause, so the original failure survives.
Miss the field and the caller ends up reading your message text to decide what to do, which breaks the first time anyone rewords it. Miss the cause and the trace stops at you.
The last question has no single right answer, and being able to argue either side is the actual skill.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Payment {
static void charge(String card, int amount)
static String attempt(String card, int amount)
static String catchOnlyArgumentProblems(String card, int amount)
static boolean isUnchecked()
}PaymentDeclinedException is a public nested class with a final declineCode field, a getter, and a constructor taking a cause. charge rejects a non positive amount with IllegalArgumentException, declines a card starting 4000 with code insufficient_funds and one starting 4001 with expired_card. attempt branches on the CODE and returns a different answer for each.
What your program must do
- Write the exception with a distinct type, a final field and a cause constructor
- Branch on the decline code rather than the message text
- Show that catching argument problems does not swallow a decline
- Decide between Exception and RuntimeException and defend it
public class Payment {
// TODO: PaymentDeclinedException. It needs:
// a distinct type, so callers can catch this and not other failures
// a decline code as a FINAL FIELD, not buried in the message
// a constructor taking a cause
static void charge(String card, int amount) {
// TODO: amount <= 0 -> IllegalArgumentException
// card starts with 4000 -> declined, code insufficient_funds
// card starts with 4001 -> declined, code expired_card
}
// Catch the decline and branch on the CODE, not on the message text.
static String attempt(String card, int amount) { return ""; } // TODO
// Catch ONLY argument problems. A decline must pass straight through.
static String catchOnlyArgumentProblems(String card, int amount) { return ""; } // TODO
static boolean isUnchecked() { return false; } // TODO
public static void main(String[] args) {
// TODO: try all three kinds of card
}
}
Hint 1
e.declineCode() keeps working when somebody rewords the message. One reading getMessage().contains(...) does not.Hint 2
final, for the same reason as Section 8.2. An exception travels up through code you do not control, and nothing on the way should be able to change what it says.Hint 3almost the answer
Checked Exceptions Meet Lambdas
ex-12-2-dTwo good designs, seventeen years apart, that cannot both be right.
Checked exceptions assume every caller is a method with a signature you can change. A lambda’s signature belongs to an interface written by somebody else, years before your method existed, and it cannot be widened without breaking everything else that uses it.
Write the workaround and notice what it costs. You are catching a checked exception purely to stop the compiler complaining, and rethrowing it as one nobody has to handle. That is the argument against checked exceptions, made by your own code.
The timing at the end is a separate lesson about where an exception’s cost actually lives.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Clash {
static boolean consumerCanDeclareChecked()
static String mightFail(String name) throws Exception
static List<String> readAllWrapping(List<String> names)
static List<String> readAllWithHelper(List<String> names)
static String causeOf(Runnable work)
static long timeThrows(int n, boolean withTrace)
static int traceLengthOf(boolean withTrace)
}mightFail throws a checked Exception for any name starting with bad, otherwise returns it uppercased. readAllWrapping uses forEach and catches inside the lambda, wrapping in an unchecked WrappedException that keeps the cause. Also declare your own ThrowingFunction interface that DOES allow a checked exception, and an unchecked helper that adapts it. FastException uses super(m, null, false, false) so it carries no trace.
What your program must do
- Show a checked exception failing to compile inside a lambda and read the error
- Explain why Consumer.accept cannot declare it
- Fix it by wrapping, and say what you gave up
- Measure the cost of the stack trace with 200,000 throws each way
import java.util.*;
import java.util.function.*;
public class Clash {
// Try: names.forEach(n -> mightFail(n)); Read the error.
// Then look at Consumer.accept and work out why it cannot be widened.
static boolean consumerCanDeclareChecked() { return true; } // TODO
static String mightFail(String name) throws Exception {
if (name.startsWith("bad")) throw new Exception("cannot read " + name);
return name.toUpperCase();
}
// TODO: an unchecked exception to wrap in, keeping the cause
static class WrappedException extends RuntimeException {
WrappedException(String message, Throwable cause) { super(message, cause); }
}
// Use forEach. Catch inside the lambda and wrap.
static List<String> readAllWrapping(List<String> names) { return List.of(); } // TODO
// TODO: your own functional interface that DOES declare a checked exception,
// plus a helper that adapts one into an ordinary Function
static List<String> readAllWithHelper(List<String> names) { return List.of(); } // TODO
static String causeOf(Runnable work) { return ""; } // TODO
// TODO: a FastException using super(m, null, false, false), so it has no trace
static long timeThrows(int n, boolean withTrace) { return 0; } // TODO
static int traceLengthOf(boolean withTrace) { return -1; } // TODO
public static void main(String[] args) {
// TODO: 200_000 throws each way, warmed up first
}
}
Hint 1
Consumer.accept was written years before your method existed. Adding throws Exception to it would break every other use of Consumer in the world.Hint 2
Function, so nothing in the standard library will accept it without an adapter.Hint 3almost the answer
After the credits
One line in this section was about memory rather than failure.
An exception carries its stack trace, and a trace holds references to the objects in those frames.
Keep a list of exceptions and you keep every object those frames were holding, long after the code that made them finished. Nothing looks wrong. The list is small. The memory is not.
Phase XIII is the one that explains that properly. What the stack and the heap actually are. Why a local variable dies when its method returns while an object does not. How the garbage collector decides which objects are still needed.
It also settles three arguments from earlier phases. “Ten million Integer objects on the heap” has been the reason to prefer IntStream twice now. “LinkedList scatters nodes across memory” was the reason ArrayList beat it in Phase X. Both of those become measurable there.
Checked exception will return in 14.10 - Stop Making Threads