Checked vs unchecked exceptions — when to use which?
MediumChecked exceptions (Exception but not RuntimeException) must be caught or declared, so the compiler forces callers to think about them. Use them for failures a caller can act on. Unchecked ones (RuntimeException, Error) signal bugs or failures nobody nearby can fix.
How it works
- The hierarchy decides. Everything throwable extends
Throwable.Error(OutOfMemoryError,StackOverflowError): unchecked, JVM-level trouble.RuntimeExceptionand its subclasses (NullPointerException,IllegalArgumentException): unchecked.- Every other
Exception(IOException,SQLException,TimeoutException): checked.
- Compile-time rule. A method that can throw a checked exception must either catch it or list it in
throws. Unchecked exceptions need neither. The JVM treats both kinds the same at runtime. - When to choose checked. The caller can reasonably recover and you want to force the decision: retry the download, ask for another file, fall back to a cache.
- When to choose unchecked.
- Programming errors: bad arguments, broken state,
nullwhere it isn't allowed. The fix is in the code, not acatch. - Failures that only a top-level handler can deal with (log, return 500).
- Programming errors: bad arguments, broken state,
- The modern lean. Lambdas and streams can't propagate checked exceptions through standard functional interfaces, and frameworks like Spring translate checked exceptions into unchecked ones. Many teams keep checked exceptions for a few clearly recoverable cases only.
Example
// Checked: the caller can decide to retry or pick another file
static String loadTemplate(Path path) throws IOException {
return Files.readString(path);
}
// Unchecked: a caller passing a negative quantity has a bug
static void reserve(String sku, int qty) {
if (qty <= 0) throw new IllegalArgumentException("qty must be positive, got " + qty);
// ...
}
// Checked exceptions don't fit inside a lambda; wrap them, keeping the cause
List<String> bodies = paths.stream()
.map(p -> {
try { return Files.readString(p); }
catch (IOException e) { throw new UncheckedIOException("cannot read " + p, e); }
})
.toList();Edge cases
- When you override a method, its
throwsclause can drop checked exceptions or list subclasses of them, but it can't add new or wider ones. Unchecked exceptions aren't limited. - With try-with-resources, a failure during cleanup never hides the original problem: the exception from the
tryblock propagates, and any exception from closing the resource rides along ingetSuppressed(). catch (Exception e)also catches everyRuntimeException, including the bugs you wanted to surface.- Generics can smuggle a checked exception out undeclared (the "sneaky throw" trick). It compiles, but callers can't catch it by type without tricks.
Common mistakes
- Empty
catchblocks, ore.printStackTrace()and carry on. - Wrapping without the cause (
new RuntimeException(e.getMessage())), which throws the original stack trace away. - Catching
ErrororThrowablein ordinary code. There's rarely a sane recovery fromOutOfMemoryError.
Likely follow-up
"How would you design exceptions for a service layer?" A small set of unchecked domain exceptions (OrderNotFoundException, PaymentDeclinedException) that carry context, thrown from the service and mapped to responses in one place, such as a Spring @ControllerAdvice.
Get every deep dive in the app
Coming soon to the App StoreComing soon to Google Play