Java exception output questions can look like guesswork, but a small set of rules decides each result. Students often remember the try and catch shape without knowing where checked exceptions end or when a finally return wins. That is enough for a two-line snippet to cause trouble.
The Throwable hierarchy
Everything that can be thrown with Java's exception mechanism descends from Throwable. Its two main branches are Error and Exception.
Error represents serious conditions such as OutOfMemoryError and StackOverflowError. Application code normally does not try to recover by catching these. Catching Throwable just to keep a program moving also catches errors and is usually far too broad.
Exception contains failures that application code may handle. Its RuntimeException branch is unchecked. Examples include:
NullPointerExceptionArithmeticExceptionArrayIndexOutOfBoundsException
The other familiar Exception subclasses, such as IOException, SQLException, and ClassNotFoundException, are checked exceptions.

The tree matters because compile-time handling rules follow the inheritance relationship, not a judgement made separately for each class name.
Checked vs unchecked exceptions in practice
A checked exception must be caught or declared with throws. If a method calls code that can throw IOException, it can handle the failure locally:
try {
String text = Files.readString(path);
use(text);
} catch (IOException e) {
reportReadFailure(e);
}Or it can state that callers must deal with it:
String load(Path path) throws IOException {
return Files.readString(path);
}Unchecked exceptions, meaning RuntimeException and its subclasses, need neither a catch nor a throws declaration. The compiler allows this because such failures often indicate a programming error, such as dereferencing null, using a bad array index, or dividing an integer by zero.
A useful design rule is to use a checked exception when the caller is reasonably expected to recover from an external condition, and an unchecked exception when the contract has been violated or the program state is invalid. It is a rule of thumb, not a replacement for understanding the API you are calling.
The same distinction comes back in a full fresher round, next to object basics, the string pool and collection choice: see Java interview questions on OOP, strings and collections.
finally in Java: the two return rules behind output questions
A finally block runs after the try and any matching catch, whether that earlier code finishes normally, returns a value, or throws. It is skipped when the JVM stops before it can run, for example through System.exit(), a JVM crash, or an external process kill.
Two compact methods expose both rules.
Case one: a return inside finally overrides
static int t() {
try {
return 1;
} finally {
return 2;
}
}Trace it in order:
The
tryblock prepares to return 1.Before the method can complete,
finallyruns.finallyexecutes its ownreturn 2.That abrupt completion replaces the pending return, so
t()returns 2.
The same masking problem applies to a throw in finally: it can replace an exception already in flight. A return or throw in finally is therefore legal but dangerous.
Case two: changing a local does not change a captured return value
static int t2() {
int x = 1;
try {
return x;
} finally {
x = 99;
}
}This method returns 1, not 99:
Java evaluates
return xand saves the primitive value 1 for return.finallyruns and changes the local variablexto 99.finallycompletes normally, so the already saved value 1 is returned.
The pair to remember is t() = 2 and t2() = 1. The first finally creates a new return that overrides the pending one. The second merely mutates a local after the original return expression has already been evaluated. The Java Language Specification says the same in Execution of try-finally and try-catch-finally: a finally block that completes abruptly decides how the whole try statement completes.
For reference values, the reference itself is saved. Mutating the referred object in finally is still visible to the caller, but reassigning the local to a different object does not replace the saved reference.
try-with-resources, suppressed exceptions and multi-catch
Try-with-resources closes each declared resource automatically, in reverse order of declaration, so the resource opened last is closed first.
try (InputStream in = Files.newInputStream(input);
OutputStream out = Files.newOutputStream(output)) {
in.transferTo(out);
}Here out is declared second, so it closes before in. If the body throws and closing a resource also throws, the body's exception remains the primary one. The close-time failure is attached as a suppressed exception and can be inspected through getSuppressed(). This preserves information that a manual finally close could accidentally mask.
Multi-catch lets one handler cover unrelated alternatives:
try {
process();
} catch (IOException | SQLException e) {
log(e);
}You cannot combine a superclass and its subclass in the same multi-catch, because the broader alternative already covers the narrower one. Catch ordering follows the same principle. This does not compile:
try {
read();
} catch (Exception e) {
handle(e);
} catch (IOException e) {
handleIo(e);
}The IOException handler is unreachable because the earlier Exception handler has already caught it. Put the specific catch first and broader catches later.
Custom exceptions, wrapping and chaining
Extend Exception when callers should be forced to catch or declare your custom failure. Extend RuntimeException when it represents an unchecked contract or state problem.
final class AppException extends RuntimeException {
AppException(String message, Throwable cause) {
super(message, cause);
}
}When translating a low-level failure into a domain-level one, preserve the original cause:
try {
repository.save(record);
} catch (SQLException e) {
throw new AppException("Could not save record", e);
}The caller sees a meaningful application message and can inspect getCause() for the original SQLException. Rethrowing the same exception preserves its type. Wrapping changes the abstraction level, so retain the cause instead of discarding the debugging trail.
Exception-handling traps to reject
Catching
ThrowableorErroras routine recovery.Leaving an empty catch block that silently swallows the failure.
Returning or throwing from
finally, which can mask a pending return or exception.Placing a broad catch before a more specific one.
Assuming
finallyis skipped merely because thetrythrows.Wrapping an exception without passing the original cause.
Language features have evolved, but these control-flow rules have not. Streams, records and sealed classes sit on top of them rather than replacing them, and Java 8 to 21 features for interviews goes through what changed.
How exception handling is tested
Three question shapes account for most of what gets asked, and each one is decided by a rule rather than by recall.
The first is “what does this print?”. The trap is the ordering between a catch that returns and a side effect in finally.
static String f() {
try {
throw new IllegalStateException("boom");
} catch (RuntimeException e) {
return "catch";
} finally {
System.out.print("finally ");
}
}IllegalStateException is a RuntimeException, so the catch matches and prepares to return "catch". Before the method completes, finally runs and prints its text. It completes normally, so the pending return survives. Calling System.out.println(f()) therefore prints finally catch on one line.
The second is “does this compile?”. Order catches from specific to broad, and never pair a class with its own superclass inside one multi-catch. Both mistakes are compile errors, not runtime surprises, so the answer is no before the program ever runs.
The third is “must this be declared?”. Answer it from the type alone. Anything under RuntimeException needs nothing. Any other Exception subclass must be caught or listed in throws. So a method calling Files.readString has to do one or the other, while a method that risks a NullPointerException does not.
The short version and your next step
Know the tree first: RuntimeException is the unchecked branch under Exception; the familiar non-runtime exception branch is checked. Then remember the classic pair: a return in finally overrides a pending return, but changing a local in finally does not change a primitive return value that was already evaluated.
Practise those three shapes in the Java course, which pairs each concept with MCQs and coding problems. Our Programming Languages question set runs to about 1,300 questions, and its Java topic alone carries roughly 280 of them across exception handling, the final keyword, methods, operators and the JVM. The Coding and DSA courses run the same practice format across data structures and algorithms, if placement rounds rather than Java alone are your target.




