A checked exception is one the compiler makes you deal with: if a method can throw it, the caller must either catch it or declare it with throws, or the code won’t compile. An unchecked exception carries no such obligation. In Java the split is by class: RuntimeException, Error and their subclasses are unchecked, and every other subclass of Throwable (in practice, Exception and its non-runtime subclasses) is checked.
IOException, SQLException and InterruptedException are checked. NullPointerException, IllegalArgumentException, IllegalStateException and ArithmeticException are unchecked. That’s the definition, and it gets you through the first question. The follow-ups test whether you know what the compiler actually enforces, where checked exceptions get awkward (lambdas and overriding), and how you’d decide which kind to throw.
The rules at a glance
| Checked | Unchecked | |
|---|---|---|
| Classes | Exception and subclasses, except RuntimeException |
RuntimeException, Error and subclasses |
| Must be caught or declared | Yes, compile error otherwise | No |
| Checked by | javac only |
Nobody |
Can appear in throws |
Yes, required if not caught | Allowed, purely documentation |
| Overriding a method | Can’t add new or broader ones | Free to throw any |
Inside Function, Supplier, etc. |
Not allowed | Allowed |
| Usually means | An expected failure outside the program’s control | A bug, broken precondition or JVM problem |
| Examples | IOException, SQLException, TimeoutException |
NullPointerException, IllegalArgumentException, UncheckedIOException |
The hierarchy is worth drawing on a whiteboard:
Throwable
├── Error unchecked (OutOfMemoryError, StackOverflowError)
└── Exception checked
├── IOException checked
├── SQLException checked
└── RuntimeException unchecked
├── NullPointerException
├── IllegalArgumentException
└── IllegalStateExceptionError sits outside Exception and is unchecked too. It represents problems the application usually can’t recover from, which is why nothing forces you to catch it.
What the compiler enforces
The examples below are illustrative Java 17; they weren’t compiled for this page, and the error messages are the ones javac prints, trimmed to the important line.
Call a method that declares a checked exception without handling it, and compilation stops:
String loadTemplate(Path path) {
return Files.readString(path);
}
// error: unreported exception IOException; must be caught or declared to be thrownYou have two ways out. Declare it, passing the obligation to your caller:
String loadTemplate(Path path) throws IOException {
return Files.readString(path);
}Or catch it and do something meaningful, such as falling back or translating it:
String loadTemplate(Path path) {
try {
return Files.readString(path);
} catch (NoSuchFileException e) {
return DEFAULT_TEMPLATE; // a real recovery
} catch (IOException e) {
throw new UncheckedIOException("Cannot read template " + path, e);
}
}The compiler also works in the other direction. Catching a checked exception that the try block can’t throw is an error (exception IOException is never thrown in body of corresponding try statement), which stops dead catch blocks from piling up. Catching Exception is always allowed, since it includes unchecked exceptions that could come from anywhere.
Unchecked exceptions get none of this. Integer.parseInt(text) can throw NumberFormatException, and the compiler says nothing whether you handle it or not.
It’s a compile-time feature only
The JVM doesn’t know about checked exceptions. At runtime, every exception unwinds the stack the same way, and bytecode can throw a checked exception from a method that never declared it. Other JVM languages rely on this: Kotlin and Scala have no checked exceptions, so a Kotlin function can throw IOException into your Java code without any throws clause. Inside Java, generic tricks (often called “sneaky throws”) and some libraries do the same.
This matters in interviews because it corrects a common misconception: checked exceptions aren’t “safer at runtime” or handled differently by the JVM. They’re a static check that a caller has thought about a failure, nothing more.
Where checked exceptions get in the way
Overriding and interfaces
An overriding method may throw fewer or narrower checked exceptions than the method it overrides, never new or broader ones. Callers holding a reference to the parent type only prepared for what the parent declared:
interface TemplateSource {
String load(String name); // declares nothing
}
class FileTemplateSource implements TemplateSource {
public String load(String name) throws IOException { // compile error
return Files.readString(Path.of(name));
}
}
// error: load(String) in FileTemplateSource cannot implement load(String) in TemplateSource
// overridden method does not throw IOExceptionThe interface’s designer decided the contract. The implementation must catch and translate, or the interface must change to declare the exception. That’s why AutoCloseable.close() declares throws Exception (implementations can narrow it) and Closeable.close() declares throws IOException.
Lambdas and streams
The standard functional interfaces (Function, Supplier, Consumer, Predicate) declare no checked exceptions, so a lambda or method reference passed to them can’t throw one:
List<String> bodies = paths.stream()
.map(Files::readString) // compile error: unreported exception IOException
.toList();The usual fix is to wrap inside the lambda and convert to an unchecked exception, keeping the cause:
List<String> bodies = paths.stream()
.map(p -> {
try {
return Files.readString(p);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.toList();UncheckedIOException exists in the JDK (since Java 8) precisely for this. If you do this often, a small helper or your own functional interface that declares throws IOException keeps the call sites readable. Expect an interviewer to ask about this one; it’s the most common day-to-day friction with checked exceptions.
How to choose which kind to throw
Ask whether a typical caller can do something useful about the failure. If yes, and it’s an expected part of the operation (a missing file a user picked, a payment declined, a remote service timing out), a checked exception documents that in the signature and forces callers to plan for it. If the failure means the caller did something wrong (passed null, called methods in the wrong order, violated a documented range), throw an unchecked exception: there’s no sensible recovery except fixing the code, and forcing every caller to write a catch for it just adds noise.
In practice, much of the Java ecosystem has drifted toward unchecked exceptions for application code. Spring translates SQLException into its unchecked DataAccessException hierarchy, JPA throws unchecked PersistenceException, and many teams define unchecked domain exceptions so that intermediate layers don’t have to declare failures they can’t act on. Checked exceptions remain common at I/O boundaries. Either position is defensible in an interview if you explain the trade-off: checked exceptions make failure visible in signatures, at the cost of verbosity and awkwardness with lambdas.
Three things to avoid whichever kind you use:
- Swallowing.
catch (IOException e) { }turns a failure into wrong behaviour later. If you catch, recover or rethrow. - Dropping the cause.
throw new ServiceException(e.getMessage())loses the stack trace. Passeas the cause. - Catching
InterruptedExceptionand moving on. Restore the flag withThread.currentThread().interrupt()so code further up knows the thread was asked to stop.
Where those catch blocks should live, and how to translate between layers, is covered in Java exceptions and recovery boundaries.
Follow-up questions you may hear
Does catch (Exception e) catch unchecked exceptions? Yes, RuntimeException is a subclass of Exception. It doesn’t catch Errors; only catch (Throwable t) does.
Can you declare an unchecked exception in throws? Yes. It’s legal and sometimes used as documentation, but the compiler ignores it for callers.
What does catch (Exception e) { throw e; } declare? Since Java 7, the compiler does precise rethrow analysis: if the try block can only throw IOException, the method needs to declare only IOException, not Exception, as long as e isn’t reassigned.
Is Exception itself checked? Yes. Declaring throws Exception forces every caller to handle or redeclare it, which is why broad throws Exception clauses are discouraged outside of tests and main.
Interview exercise: classify and fix
A colleague writes a ConfigLoader that implements Supplier<Config>. Its get() reads a YAML file with a method declared as Config parse(Path p) throws IOException and throws IllegalArgumentException when the file’s port is outside 1 to 65535. The code doesn’t compile. Which exception is the problem, how do you fix it, and are both exception types well chosen?
Answer
The IOException is the problem. It’s checked, and Supplier.get() declares no checked exceptions, so the implementation can’t let it escape (and can’t add a throws clause, because overriding methods can’t introduce new checked exceptions). IllegalArgumentException is unchecked, so it compiles fine.
Fix it by catching inside get() and wrapping with the cause, throw new UncheckedIOException("Cannot read " + path, e), or by not implementing Supplier and exposing a plain Config load() throws IOException method so callers see the failure in the signature. The second is better if callers are expected to handle a missing config (for example, by falling back to defaults); the first suits startup code where the only reaction is to fail fast.
On the choices: wrapping I/O failure is reasonable. IllegalArgumentException is a weak fit, because the bad port isn’t an argument the caller passed, it’s invalid file content. A dedicated unchecked InvalidConfigException naming the file and the field would be clearer, and it would let a startup handler print a precise message instead of a generic stack trace.