Checked and unchecked exceptions
Java splits exceptions into two kinds. The compiler forces you to handle checked exceptions in advance, otherwise the program won't compile. Unchecked exceptions can be left unhandled — the compiler says nothing about them. This is specific to Java: most other languages have no such split.
Which is which
The rule is simple:
- Unchecked —
RuntimeException, all its subclasses, and alsoError. These are usually mistakes in the program's logic:NullPointerException,ArithmeticException,IllegalArgumentException. - Checked — all other subclasses of
Exception. These are situations the programmer doesn't control: the file is missing, the network is down. The best-known example isIOException, an input/output error.
The Files.readString method reads a file and may throw IOException. That's why the compiler won't let you call it without handling:
void main() {
String text = Files.readString(Path.of("notes.txt"));
IO.println(text);
}Output:
Main.java:2: error: unreported exception IOException; must be caught or declared to be thrown
String text = Files.readString(Path.of("notes.txt"));
^
1 error
error: compilation failedOption 1: catch it
The first way to satisfy the compiler is to wrap the call in try-catch:
void main() {
try {
String text = Files.readString(Path.of("notes.txt"));
IO.println(text);
} catch (IOException e) {
IO.println("Could not read the file: " + e.getMessage());
}
}Output:
Could not read the file: notes.txtThere is no notes.txt file, so catch ran. The IOException message here is just the file name.
Option 2: declare throws
The second way is to state honestly in the method declaration that it may throw the exception. The throws keyword does this. Then whoever calls the method handles the error:
String readNotes() throws IOException {
return Files.readString(Path.of("notes.txt"));
}
void main() {
try {
IO.println(readNotes());
} catch (NoSuchFileException e) {
IO.println("No such file: " + e.getMessage());
} catch (IOException e) {
IO.println("Some other I/O error");
}
}Output:
No such file: notes.txtNoSuchFileException is a subclass of IOException, so it can be caught separately. You can put throws on main too — then on an error the program simply ends with a stack trace.
Unchecked exceptions
For RuntimeException and its subclasses the compiler requires nothing. A method can throw them without any throws:
int percent(int part, int total) {
if (total <= 0) {
throw new IllegalArgumentException("total must be greater than zero");
}
return part * 100 / total;
}
void main() {
IO.println(percent(3, 4) + "%");
}Output:
75%Unchecked exceptions are usually not caught; instead the code is fixed so they don't happen: check for null, array bounds, input data.
Wrapping in an unchecked exception
Sometimes a checked exception can't be passed on — for example, inside a lambda, which can't declare throws. Then it gets wrapped in an unchecked one. For IOException there is a ready-made class, UncheckedIOException:
void main() {
List<String> names = List.of("a.txt", "b.txt");
try {
names.forEach(name -> {
try {
IO.println(Files.readString(Path.of(name)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (UncheckedIOException e) {
IO.println("Error: " + e.getCause().getClass().getSimpleName());
}
}Output:
Error: NoSuchFileExceptionThe original exception is kept as the cause and is available through getCause().
Common mistake
An empty catch written only to silence the compiler hides the error:
void main() {
String text = "";
try {
text = Files.readString(Path.of("config.txt"));
} catch (IOException e) {
}
IO.println("Settings: [" + text + "]");
}Output:
Settings: []The program went on with empty settings, and nobody found out why. If there is nothing sensible to do on the spot, add throws and let the calling code deal with it — and in a catch at least print the message.