Exceptions
Checked vs unchecked exceptions, throw vs throws, catch ordering and multi-catch, exception chaining, try-with-resources and suppressed exceptions.
15 questions
JuniorTheoryVery commonHow are Throwable, Error, Exception, and RuntimeException related in Java?
How are Throwable, Error, Exception, and RuntimeException related in Java?
Throwable is the root of everything that can be thrown or caught. It has two direct subclasses: Error, which signals serious JVM-level failures like OutOfMemoryError you are not meant to catch, and Exception, which models application problems. Exception's own subclass RuntimeException is unchecked, while every other Exception is checked.
Common mistakes
- ✗Thinking
Erroris a normalExceptionyou should catch and recover from - ✗Placing
RuntimeExceptionaboveExceptioninstead of below it in the hierarchy - ✗Assuming every
Exceptionsubclass is checked, forgettingRuntimeExceptionis unchecked
Follow-up questions
- →Which single class must you subclass to create your own throwable type?
- →Why does catching
Throwablerisk swallowing anOutOfMemoryError?
JuniorTheoryVery commonWhat is the difference between throw and throws in Java?
What is the difference between throw and throws in Java?
throw is a statement inside a method body that actually raises a single exception instance right now, e.g. throw new IOException(). throws is a clause in the method signature that declares which checked exceptions the method may propagate to its caller, so the caller knows to catch or re-declare them. One acts; the other documents.
Common mistakes
- ✗Writing
throwsinside the method body orthrowin the signature, mixing up statement and clause - ✗Believing
throwcan raise more than one exception instance in a single statement - ✗Thinking the
throwsclause is needed for unchecked exceptions likeRuntimeException
Follow-up questions
- →Does a method need a
throwsclause for the unchecked exceptions it may raise? - →Can a
throwsclause declare a supertype likeExceptioninstead of the specific subclass?
MiddleTheoryVery commonWhat is the difference between checked and unchecked exceptions in Java?
What is the difference between checked and unchecked exceptions in Java?
Checked exceptions are subclasses of Exception excluding RuntimeException; the compiler forces every caller to either catch them or declare them with throws, so they model recoverable conditions like a missing file. Unchecked exceptions, subclasses of RuntimeException and Error, need no declaration and usually signal programming bugs or fatal conditions you are not expected to recover from.
Common mistakes
- ✗Thinking
ErrorandRuntimeExceptionare checked and require athrowsclause - ✗Believing the checked/unchecked split is decided at runtime rather than by class hierarchy
- ✗Catching a broad checked
Exceptionjust to silence the compiler instead of handling it
Follow-up questions
- →Why are
RuntimeExceptionandErrorleft unchecked by the language designers? - →When is it justified to wrap a checked exception in an unchecked one and rethrow?
JuniorCodeCommonOrder the catch clauses so a missing file and a general I/O failure are both handled
Order the catch clauses so a missing file and a general I/O failure are both handled
Java scans the catch clauses top-down and enters the first one whose type is assignable from the thrown exception, so the subclass has to come first: catch (FileNotFoundException e) and only then catch (IOException e). In the opposite order the broad clause already handles the subclass, the narrow one becomes unreachable, and the compiler rejects it outright.
Common mistakes
- ✗Putting
catch (Exception e)first and a narrower clause below it - ✗Believing
catchclauses are matched by best fit, like overload resolution - ✗Thinking an unreachable
catchis a warning rather than a compile error
Follow-up questions
- →What changes if the two exception types are unrelated instead of subclass and superclass?
- →How does a multi-catch clause interact with these ordering rules?
JuniorCodeCommonOpen two resources in one try-with-resources and determine their close order
Open two resources in one try-with-resources and determine their close order
Declare both resources in one try header, separated by a semicolon. They are closed in reverse order of declaration, so the program prints open A, open B, close B, close A. Reverse order is guaranteed by the language, because a later resource is often built on an earlier one. Every declared resource is closed even if a previous close() threw — that failure is not lost, it is recorded on the propagating exception as a suppressed one.
Common mistakes
- ✗Expecting resources to close in declaration order rather than in reverse
- ✗Assuming a failing
close()on one resource leaves the others unclosed - ✗Adding a
finallywith a manualclose()on top of try-with-resources
Follow-up questions
- →Why does the language close the later resource first instead of the earlier one?
- →Where does a
close()failure go when thetrybody has already thrown?
JuniorTheoryCommonWhat does try-with-resources do, and how does it manage cleanup?
What does try-with-resources do, and how does it manage cleanup?
It declares one or more resources in parentheses after try, and the JVM closes them automatically at the end of the block — in reverse order of opening — by calling close() on each. The resource must implement AutoCloseable. Closing happens whether the block exits normally or via an exception, so you never write a finally to release files or streams. If both the body and close() throw, the close() failure becomes a suppressed exception.
Common mistakes
- ✗Believing the resource needs no interface, when it must implement
AutoCloseable(orCloseable) - ✗Thinking resources close in opening order rather than reverse order
- ✗Assuming a
close()failure overwrites the body exception instead of being suppressed
Follow-up questions
- →What interface must a resource implement to be used in try-with-resources?
- →What is a suppressed exception, and how do you retrieve it?
MiddleCodeCommonRethrow a SQLException as a domain exception without losing the original cause
Rethrow a SQLException as a domain exception without losing the original cause
Give the custom type a (String, Throwable) constructor that forwards to super(message, cause), then rethrow with throw new UserLoadException(message, e);. Throwable itself stores that cause, so getCause() returns the SQLException and printStackTrace prints its frames under Caused by:. Passing only e.getMessage() into a message-only constructor keeps the text and silently discards the original stack trace.
Common mistakes
- ✗Rethrowing with
e.getMessage()only, which drops the original stack trace - ✗Assuming the cause is linked automatically because the throw sits inside a
catch - ✗Confusing
addSuppressedwithinitCause— suppression is not chaining
Follow-up questions
- →When would you use
initCause()instead of a constructor taking the cause? - →How deep can a chain of causes go, and how do you walk it programmatically?
MiddleTheoryCommonWhat is a suppressed exception in try-with-resources, and how do you read it?
What is a suppressed exception in try-with-resources, and how do you read it?
If the try body throws and close() then throws too while the block unwinds, the body's exception is the one that propagates, and the close() failure is attached to it by addSuppressed() instead of replacing it. You read those secondary failures back with Throwable.getSuppressed(), and printStackTrace lists them under Suppressed:. A hand-written try/finally has the opposite bug — an exception from the finally replaces the original.
Common mistakes
- ✗Thinking the
close()failure wins and the body exception is the suppressed one - ✗Believing a
close()failure is silently discarded rather than attached - ✗Confusing
getSuppressed()withgetCause()— suppression is not chaining
Follow-up questions
- →How can a hand-written
try/finallylose the original exception entirely? - →Can you call
addSuppressed()yourself outside try-with-resources, and when is that useful?
JuniorTheoryOccasionalWhat does a multi-catch clause do, and what restrictions does it impose?
What does a multi-catch clause do, and what restrictions does it impose?
One catch clause may list several exception types separated by | — catch (IOException | SQLException e) — so a shared handler body is written once instead of copied per type. The alternatives may not stand in a subtype relationship with each other: catch (IOException | FileNotFoundException e) is a compile error. The parameter is implicitly final, and its static type is the nearest common supertype of the listed alternatives.
Common mistakes
- ✗Listing a type and its own subtype as two alternatives in one clause
- ✗Trying to reassign the multi-catch parameter, which is implicitly
final - ✗Expecting the parameter to have the dynamic type instead of the common supertype
Follow-up questions
- →What is the static type of
eincatch (IOException | SQLException e)? - →How does precise rethrow let a method declare narrower types than
Exception?
SeniorTheoryOccasionalShould you ever catch an Error, and what breaks when you do?
Should you ever catch an Error, and what breaks when you do?
Almost never. Error marks a JVM-level failure the application is not expected to recover from: after an OutOfMemoryError or a StackOverflowError the heap or the stack is already compromised, so a handler that carries on runs on untrustworthy state. The narrow legitimate uses are a top-level handler that logs and then exits, and a framework isolating one task. The real hazard is catch (Throwable t) in ordinary code — it swallows every Error silently.
Common mistakes
- ✗Treating
OutOfMemoryErroras a recoverable condition you retry after freeing a cache - ✗Writing
catch (Throwable t)for a broad catch, which swallows everyErrortoo - ✗Assuming an
Erroris confined to its thread and leaves the rest of the JVM healthy
Follow-up questions
- →Why is
catch (Throwable t)more dangerous thancatch (Exception e)in a request handler? - →What should a thread pool do when a worker task dies with an
OutOfMemoryError?
SeniorDesignOccasionalYou are reviewing the exception design of a three-layer service — a JDBC repository, a domain service, and a REST controller. Today the repository lets a raw SQLException escape, the service catches it and rethrows new RuntimeException(e.getMessage()), and the controller maps anything it catches to HTTP 500. Product now needs 404 for a missing entity, 409 for a uniqueness violation, and 500 only for genuine faults, while support needs the original database error visible in the logs. Constraints: no new library may be added, the repository must not know anything about HTTP, and the number of exception types has to stay small enough for the team to remember. Propose the exception hierarchy and the translation rule at each boundary, and say what a layer must do with a failure it cannot handle itself.
You are reviewing the exception design of a three-layer service — a JDBC repository, a domain service, and a REST controller. Today the repository lets a raw SQLException escape, the service catches it and rethrows new RuntimeException(e.getMessage()), and the controller maps anything it catches to HTTP 500. Product now needs 404 for a missing entity, 409 for a uniqueness violation, and 500 only for genuine faults, while support needs the original database error visible in the logs. Constraints: no new library may be added, the repository must not know anything about HTTP, and the number of exception types has to stay small enough for the team to remember. Propose the exception hierarchy and the translation rule at each boundary, and say what a layer must do with a failure it cannot handle itself.
Give the domain a small unchecked hierarchy — AppException with NotFoundException and ConflictException — and translate at each boundary. The repository catches SQLException and rethrows the domain type with the original as its cause, so the database error still shows up under Caused by:. The controller maps those types onto 404/409/500 and never sees SQLException, keeping HTTP out of the lower layers. A layer that cannot act on a failure lets it propagate rather than swallowing it.
Common mistakes
- ✗Letting
SQLExceptionreach the controller, which couples the web layer to the driver - ✗Rethrowing with
e.getMessage()only, so the original SQL failure vanishes from the log - ✗Catching, logging and swallowing at every layer, leaving the caller unable to react
Follow-up questions
- →Should the domain exceptions be checked or unchecked here, and what decides it?
- →Where would you put the single place that turns a domain exception into an HTTP response?
SeniorDesignRareYour team is fixing the exception policy for a new public Java library. One camp wants every recoverable failure declared as a checked exception, arguing that the compiler then forces each caller to acknowledge it. The other camp points out that no mainstream language designed after Java has copied the idea — Kotlin, C# and Scala leave every exception unchecked — and wants the same here. Constraints: the library's methods are routinely called from lambdas passed to Stream operations; at least three internal frameworks wrap the library and re-expose its failures; a new minor version ships every month and must stay source-compatible with the previous one. Argue both sides in this specific setting, name the concrete failure modes each choice produces in real code, and state the policy you would adopt and exactly where you would draw the line.
Your team is fixing the exception policy for a new public Java library. One camp wants every recoverable failure declared as a checked exception, arguing that the compiler then forces each caller to acknowledge it. The other camp points out that no mainstream language designed after Java has copied the idea — Kotlin, C# and Scala leave every exception unchecked — and wants the same here. Constraints: the library's methods are routinely called from lambdas passed to Stream operations; at least three internal frameworks wrap the library and re-expose its failures; a new minor version ships every month and must stay source-compatible with the previous one. Argue both sides in this specific setting, name the concrete failure modes each choice produces in real code, and state the policy you would adopt and exactly where you would draw the line.
Checked exceptions buy a compiler-enforced contract, but they do not compose: a checked type infects every caller's signature, cannot escape a lambda handed to Stream.map (the functional interfaces declare no throws), and adding one in a minor release breaks source compatibility. That pressure produces the two familiar anti-patterns — the empty catch (Exception e) and a blanket throws Exception. Policy: unchecked by default, checked only where a caller can realistically recover.
Common mistakes
- ✗Claiming checked exceptions cost more at run time — the split is purely a compile-time rule
- ✗Forgetting that
Function,Supplierand friends declare nothrows, so a checked type cannot leave a lambda - ✗Treating an added checked exception as a compatible change, when it breaks every existing caller
Follow-up questions
- →How do libraries make a checked exception usable inside a
Streampipeline anyway? - →Which failures in this library would still justify a checked type under your policy?
SeniorTheoryRareWhy is using exceptions for normal control flow costly?
Why is using exceptions for normal control flow costly?
Most of the cost is fillInStackTrace, run when the exception is constructed: it walks the call stack to capture every frame, which is far more expensive than a branch. So replacing an if null-check with catching a NullPointerException runs orders of magnitude slower under load and obscures intent. If a trace is genuinely unneeded you can override fillInStackTrace or use -XX:-StackTraceInThrowable, but the real fix is to test conditions explicitly.
Common mistakes
- ✗Attributing the cost to the
tryblock instead of the throw-time stack-trace capture - ✗Assuming the stack trace is captured lazily rather than at construction
- ✗Treating exceptions as a free, idiomatic substitute for an explicit condition check
Follow-up questions
- →How does overriding
fillInStackTracechange the cost of a frequently thrown exception? - →When is a preallocated singleton exception (no trace) a legitimate optimization?
SeniorTheoryRareWhat was the finalize() method, and why is it discouraged in modern Java?
What was the finalize() method, and why is it discouraged in modern Java?
finalize() was an Object hook the garbage collector called before reclaiming an object, meant for last-chance cleanup. It is discouraged because the GC runs it non-deterministically — possibly never — it slows collection, can resurrect an object by leaking this, and may mask exceptions it throws. Deprecated since Java 9, it is replaced by try-with-resources for deterministic release and Cleaner for backstop cleanup.
Common mistakes
- ✗Treating
finalize()as deterministic cleanup like a C++ destructor that runs at a known time - ✗Releasing files or sockets in
finalize()instead of try-with-resources orCleaner - ✗Assuming
finalize()is always called; the GC may never run it before exit
Follow-up questions
- →How does object resurrection in
finalize()corrupt the garbage collector's lifecycle? - →How does
Cleaneravoid the resurrection and exception-masking pitfalls offinalize()?
SeniorDesignRareYou inherit a twelve-year-old billing module. Every method in it declares throws Exception; there are forty catch (Exception e) { e.printStackTrace(); } blocks and several empty catch blocks holding only a // TODO comment; in one place a catch (SQLException e) rethrows a BillingException built from a bare message with no cause attached; and every stream is released by a hand-written finally that calls close() directly. Test coverage is thin, and eight other modules call into this one and must keep compiling throughout. You have two sprints. Describe how you would review and clean this up: what you change first, what you deliberately leave alone, how you remove throws Exception from the signatures without altering behaviour, and how you would prove afterwards that the cleanup did not swallow a failure that used to surface.
You inherit a twelve-year-old billing module. Every method in it declares throws Exception; there are forty catch (Exception e) { e.printStackTrace(); } blocks and several empty catch blocks holding only a // TODO comment; in one place a catch (SQLException e) rethrows a BillingException built from a bare message with no cause attached; and every stream is released by a hand-written finally that calls close() directly. Test coverage is thin, and eight other modules call into this one and must keep compiling throughout. You have two sprints. Describe how you would review and clean this up: what you change first, what you deliberately leave alone, how you remove throws Exception from the signatures without altering behaviour, and how you would prove afterwards that the cleanup did not swallow a failure that used to surface.
Fix the silent failures first: an empty catch and a bare printStackTrace both lose the error, so each becomes a logged rethrow or a commented decision to ignore. Restore causes next — pass the caught exception into the BillingException constructor — a missing cause makes an incident unresolvable. Only then narrow throws Exception, one leaf method at a time, so callers keep compiling. Prove it with tests asserting the exception type and its cause, not just that a call failed.
Common mistakes
- ✗Rewriting the signatures first, before the swallowed failures that actually hide bugs
- ✗Treating
printStackTraceas adequate error handling because the text lands somewhere - ✗Claiming a green build proves nothing was swallowed, without a test asserting type and cause
Follow-up questions
- →Which of the forty
catchblocks would you deliberately leave untouched, and why? - →How do you narrow
throws Exceptionon a method eight other modules already call?