Java Exception Handling
Interview Questions — Theory, Scenario-Based &
Advanced
A complete topic-wise guide for backend developers (2-4 years experience) preparing for startup
and product-based company interviews. Covers exception hierarchy, checked vs unchecked
design, try-with-resources, custom exceptions, global handling in Spring Boot, and tricky
output-based questions — each with a clear answer/approach.
1. Exception Hierarchy & Fundamentals
2. Checked vs Unchecked Exceptions
3. try-catch-finally Mechanics
4. try-with-resources & AutoCloseable
5. Custom Exceptions & Exception Chaining
6. Exception Handling in Streams & Lambdas
7. Global Exception Handling in Spring Boot (REST APIs)
8. Real-World Scenario Questions
9. Tricky Output-Based Questions
1. Exception Hierarchy & Fundamentals
Q1. What is the exception hierarchy in Java (Throwable, Exception, Error)?
Tests: Fundamentals
Throwable is the root class, with two main branches: Error (serious problems an application usually
shouldn't try to catch — e.g., OutOfMemoryError, StackOverflowError, caused by JVM/environment issues)
and Exception (conditions an application can reasonably handle). Exception further splits into checked
exceptions (must be declared or caught, subclasses of Exception excluding RuntimeException) and
RuntimeException (unchecked, don't need to be declared).
Throwable
|-- Error (OutOfMemoryError, StackOverflowError)
|-- Exception
|-- IOException (checked)
|-- SQLException (checked)
|-- RuntimeException (unchecked)
|-- NullPointerException
|-- ArrayIndexOutOfBoundsException
|-- IllegalArgumentException
Q2. What is the difference between an Exception and an Error? Should you ever catch
an Error?
Tests: Fundamentals
Exceptions represent recoverable conditions an application is expected to handle (bad input, missing file,
network timeout). Errors represent serious, usually unrecoverable problems originating from the JVM or
environment itself (running out of memory, stack overflow, linkage errors). You generally should not catch
Errors — attempting to recover from an OutOfMemoryError, for instance, rarely succeeds since the JVM
itself is in a compromised state; let the application fail fast and be restarted/monitored instead.
Q3. What is the difference between throw and throws?
Tests: Fundamentals
throw is a statement used inside a method body to actually raise/instantiate an exception at a specific point
(throw new IllegalArgumentException("bad input")). throws is a keyword used in a method signature to
declare that the method might propagate a checked exception to its caller, who must then handle or
re-declare it (public void readFile() throws IOException).
2. Checked vs Unchecked Exceptions
Q4. What is the difference between checked and unchecked exceptions? Give two
examples of each.
Tests: Fundamentals — asked in almost every interview
Checked exceptions are checked by the compiler at compile time — the caller must either catch them or
declare them with throws. Examples: IOException, SQLException. Unchecked exceptions
(RuntimeException and its subclasses) are not checked at compile time — they represent programming
errors that could theoretically happen anywhere, so forcing explicit handling everywhere would be
impractical. Examples: NullPointerException, IllegalArgumentException,
ArrayIndexOutOfBoundsException.
Q5. Should a 'user not found' scenario in a service layer use a checked or unchecked
exception? Why?
Tests: Judgment question, not just definitions
In modern backend design, most teams use a custom unchecked exception (e.g., UserNotFoundException
extends RuntimeException). Reasons: checked exceptions force every caller up the chain to declare or
catch them, get awkward inside Streams/lambdas (functional interfaces can't declare checked exceptions),
and clutter method signatures. An unchecked exception can instead be caught centrally at the API
boundary (e.g., Spring's @ControllerAdvice) and mapped to an HTTP 404 without polluting every layer in
between with explicit try-catch or throws.
Q6. Why has the industry largely moved away from checked exceptions since early
Java (e.g., in modern APIs and libraries)?
Tests: Design philosophy — common at product companies
Checked exceptions were meant to force robust error handling, but in practice they often lead to either
swallowed exceptions (empty catch blocks just to satisfy the compiler) or verbose throws declarations that
leak implementation details up unrelated layers. They also don't compose well with lambdas and Streams
introduced in Java 8. Many modern libraries and frameworks (including most of Spring) favor unchecked
exceptions combined with centralized handling, or explicit Optional/Result-style return types for genuinely
expected outcomes.
3. try-catch-finally Mechanics
Q7. Can you have a try block without a catch, only finally?
Tests: Fundamentals
Yes — try-finally is valid without any catch block. This is commonly used purely for guaranteed cleanup
(e.g., closing a resource) when you don't want to handle the exception at that level, just let it propagate after
cleanup runs.
try {
riskyOperation();
} finally {
cleanup(); // always runs
}
Q8. If both the try block and the finally block have a return statement, which one wins?
Tests: Classic trick question
The finally block's return always wins — it overrides/suppresses any return (or even an exception) from
the try block. This is considered bad practice precisely because it's confusing and can silently swallow
exceptions; avoid putting return statements inside finally.
static int test() {
try {
return 1;
} finally {
return 2; // this wins — method returns 2
}
}
Q9. Can you catch multiple exception types in a single catch block? How?
Tests: Fundamentals — multi-catch (Java 7+)
Yes, using the multi-catch syntax with a pipe (|) separator, useful when different exception types need the
same handling logic. Note: the exception variable is implicitly final, and you cannot combine exception types
that are in a subtype relationship with each other in the same multi-catch (it would be redundant).
try {
process();
} catch (IOException | SQLException e) {
[Link]("Operation failed", e);
throw new ServiceException(e);
}
Q10. What is the order of execution when a try block has multiple catch blocks for a
hierarchy of exceptions (e.g., catching both IOException and its subclass
FileNotFoundException)?
Tests: Trap question
Catch blocks are evaluated top to bottom, and the first matching catch block handles the exception. If a
more general exception type (e.g., IOException) is listed before a more specific subclass
(FileNotFoundException), the specific catch block becomes unreachable and the code won't even compile
— the compiler flags this as an error. Always order catch blocks from most specific to most general.
4. try-with-resources & AutoCloseable
Q11. What is try-with-resources, and what interface must a resource implement to be
used with it?
Tests: Fundamentals
Introduced in Java 7, try-with-resources automatically closes any resource declared in the try(...)
parentheses once the block exits — normally or via exception — without needing an explicit finally block.
The resource must implement AutoCloseable (or its subtype Closeable, used by I/O classes). Multiple
resources are closed in the reverse order of their declaration.
try (BufferedReader br = new BufferedReader(new FileReader("[Link]"))) {
return [Link]();
} // [Link]() called automatically, even on exception
Q12. If an exception occurs both inside the try block and while closing the resource (in
close()), which exception is actually thrown to the caller?
Tests: Deeper trick question
The exception from the try block body is the one propagated to the caller; the exception thrown by close()
is suppressed and attached to the primary exception, retrievable via getSuppressed() on the thrown
exception. This avoids silently losing the more important original failure.
Q13. Why is try-with-resources generally preferred over manually closing resources in
a finally block?
Tests: Best practices
Manual closing in finally is verbose and error-prone — a common bug is forgetting a null check before
calling close(), or the close() call itself throwing and masking the original exception. try-with-resources
handles all of this automatically, is far less code, and correctly manages suppressed exceptions as
described above.
5. Custom Exceptions & Exception Chaining
Q14. How do you create and use a custom exception in Java?
Tests: Fundamentals
Extend RuntimeException (unchecked, generally preferred in modern backend code) or Exception
(checked), typically providing constructors that accept a message and/or a cause. Custom exceptions let
you attach domain-specific context and enable centralized handling based on exception type (e.g., in
Spring's @ExceptionHandler).
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
}
}
Q15. What is exception chaining, and why is it important to preserve the original cause
when wrapping an exception?
Tests: Best practices, often tested in code review discussions
Exception chaining means passing the original exception as the cause when throwing a new one (e.g., new
ServiceException("failed", originalException)). This preserves the full stack trace and root cause for
debugging. A common anti-pattern is catching an exception and throwing a new one without the cause —
this silently discards the original stack trace, making production issues far harder to diagnose.
try {
[Link](order);
} catch (SQLException e) {
// GOOD: preserves original cause
throw new OrderPersistenceException("Failed to save order", e);
// BAD: throw new OrderPersistenceException("Failed to save order");
}
Q16. What is a good general strategy for designing an exception hierarchy in a
mid-size backend application?
Tests: Design/architecture question
Define a small hierarchy under a common base (e.g., ApplicationException extends RuntimeException),
then domain-specific subclasses (ResourceNotFoundException, ValidationException,
ExternalServiceException, ConflictException). Attach a stable error code and HTTP-status-friendly category
to each so a single centralized handler can map exception type to response consistently, instead of
scattering try-catch blocks with duplicated response-building logic everywhere.
6. Exception Handling in Streams & Lambdas
Q17. You need to process a list of orders and, for each, compute a discount or throw a
business exception — using Streams. How do you handle checked exceptions inside a
lambda?
Tests: Very commonly asked once teams adopt Java 8+ style code
Functional interfaces like Function<T,R> don't declare checked exceptions in their abstract method
signature, so a lambda can't throw a checked exception directly. Common approaches: (1) wrap the
checked exception in an unchecked one inside the lambda body and rethrow, (2) write a custom functional
interface that declares the checked exception and use a small utility to adapt it, or (3) simply avoid checked
exceptions in the domain layer altogether (ties back to Q5/Q6).
[Link]()
.map(order -> {
try {
return computeDiscount(order); // throws checked BusinessException
} catch (BusinessException e) {
throw new RuntimeException(e); // wrap as unchecked
}
})
.collect([Link]());
Q18. If an exception is thrown partway through a Stream pipeline (e.g., inside map()),
what happens to the elements already processed and the ones not yet processed?
Tests: Trap/behavior question
The entire stream pipeline is aborted immediately when an unhandled exception propagates out of a
lambda — Streams don't offer partial results or continue-on-error semantics by default. Any results already
collected are lost since collect() never completes. If partial success/failure tracking is needed, you must
explicitly capture successes/failures per element (e.g., into a Result wrapper) rather than letting exceptions
propagate out of the pipeline.
7. Global Exception Handling in Spring Boot (REST APIs)
Q19. Your REST API is swallowing exceptions and returning generic 500 errors with no
useful info to the client or logs. How do you redesign the error handling?
Tests: Extremely common real-world scenario at product companies
Implement centralized exception handling using @ControllerAdvice combined with @ExceptionHandler
methods for each exception type (or a hierarchy). Return a structured error response — error code,
human-readable message, timestamp, and a trace/correlation ID — instead of a raw stack trace. Log full
stack traces server-side only (never leak internals to the client), and map custom exceptions to appropriate
HTTP status codes (404 for not-found, 400 for validation errors, 409 for conflicts, 500 as a last-resort
fallback for truly unexpected errors).
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler([Link])
public ResponseEntity handleNotFound(OrderNotFoundException ex) {
return [Link](HttpStatus.NOT_FOUND)
.body(new ErrorResponse("ORDER_NOT_FOUND", [Link]()));
}
@ExceptionHandler([Link])
public ResponseEntity handleGeneric(Exception ex) {
[Link]("Unhandled exception", ex);
return [Link](HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "Something went wrong"));
}
}
Q20. How would you handle validation errors (e.g., @Valid on a request DTO failing)
globally instead of manually checking each field in every controller?
Tests: Common Spring Boot follow-up
Let Bean Validation annotations (@NotNull, @Size, @Email, etc.) on the request DTO do the validation
automatically via @Valid in the controller method signature. When validation fails, Spring throws
MethodArgumentNotValidException, which you catch once in the @ControllerAdvice to build a structured
response listing all failed fields — instead of duplicating manual if-checks across every endpoint.
Q21. Why is it bad practice to catch a generic Exception (or even Throwable) at a low
level just to log it and continue, without rethrowing?
Tests: Code review / best practices scenario
This is often called "swallowing" an exception — it hides real failures from callers, can leave the application
in an inconsistent state (e.g., a partially completed transaction that silently continues), and makes
production issues far harder to trace since the failure never surfaces where it matters. Exceptions should
generally be handled at a layer that can meaningfully act on them (retry, fallback, or surface to the caller) —
not swallowed just to avoid a compiler warning or crash.
8. Real-World Scenario Questions
Q22. A scheduled batch job processes 1000 records, but if record #500 throws an
exception, the whole job stops and records 501-1000 never get processed. How would
you redesign this?
Tests: Real production scenario
Wrap the processing of each individual record in its own try-catch so a single failure doesn't halt the entire
batch. Collect failures into a list (record ID + error) for reporting or retry, and continue processing the
remaining records. Consider a dead-letter mechanism (separate table/queue) for records that repeatedly
fail, so they can be investigated or retried later without blocking the whole batch.
List failedIds = new ArrayList<>();
for (Record record : records) {
try {
process(record);
} catch (Exception e) {
[Link]("Failed to process {}", [Link](), e);
[Link]([Link]());
}
}
Q23. Your microservice calls an external payment API, which occasionally times out.
Should you catch the timeout exception and retry immediately, or is there a better
approach?
Tests: Resilience-focused scenario
Immediate retries can worsen an already struggling downstream service (retry storms). Better approach:
retry with exponential backoff and a max retry count, combined with a circuit breaker (e.g., Resilience4j)
that stops calling the failing service entirely for a cooldown period once failures cross a threshold, falling
back to a cached response or a graceful degradation path instead of repeatedly throwing exceptions at the
caller.
Q24. A junior developer's code catches NullPointerException everywhere instead of
fixing the root cause. What's wrong with this approach, and what should be done
instead?
Tests: Code review scenario
Catching NullPointerException as a control-flow mechanism treats a programming bug as an expected
condition, hiding the actual root cause (a missing null check, bad initialization order, or an unexpected null
from an external call) and making the code fragile and hard to reason about. Better: proactively
validate/guard against nulls at the boundary (e.g., [Link], Optional, @NonNull annotations,
defensive checks) so NPEs are prevented rather than caught and papered over after the fact.
9. Tricky Output-Based Questions
Q25. What happens if you rethrow the caught exception variable 'e' after modifying its
message — does the original stack trace change?
Tests: Trap question
If you simply do 'throw e;' after catching, the original stack trace is preserved (it was captured when the
exception was first constructed via fillInStackTrace(), not when thrown again). But if you construct a
brand-new exception (even wrapping the same message) without passing 'e' as the cause, the original
stack trace context is lost.
Q26. What is the output/behavior if a static initializer block throws an exception when a
class is first loaded?
Tests: JVM class-loading trap question
It results in an ExceptionInInitializerError (an Error, not a plain Exception), and the class fails to initialize.
Critically, the JVM marks the class as unusable — any subsequent attempt to use that class in the same
JVM run throws NoClassDefFoundError, even though the class file itself is perfectly valid; only the first
failed initialization attempt shows the real ExceptionInInitializerError.
Q27. Does a finally block execute if [Link]() is called inside the try block?
Tests: Classic trick question
No. [Link]() terminates the JVM immediately and unconditionally — the finally block is not executed in
this case, unlike almost every other way a try block can exit (normal completion, exception, return, break,
continue). This is one of the very few documented exceptions to finally's "always runs" guarantee.
Q28. What is the output when a catch block itself throws a new exception — does the
corresponding finally block still run?
Tests: Trap question
Yes — finally always runs before the newly thrown exception (or any exception/return) actually leaves the
enclosing try-catch-finally construct, regardless of whether it was thrown from try or from catch. The only
exception to this rule is [Link]() (see Q27) or the JVM crashing/being killed.
Tip: Interviewers at product companies care most about exception design philosophy — checked vs unchecked
judgment, centralized handling, and never silently swallowing failures — far more than memorized definitions
alone.