Static fields and serialization, failures during serialization, non-serializable members, NoClassDefFoundError vs ClassNotFoundException, exceptions in static initialisers, choosing checked vs unchecked exceptions, real finally-block uses and surprises, and why catching Throwable is dangerous.
Published September 25, 2026
Intermediate exception questions are about design choices and failure modes you've debugged. "Have you ever…?" questions invite a short real-world story: the situation, what went wrong, and what you changed. Keep one or two ready.
Short answer: No. Serialization captures instance state, and static fields belong to the class. On deserialization, a static field simply has whatever value the class currently holds in the receiving JVM.
Key points to cover:
serialVersionUID. It's a private static final field that the serialization mechanism reads specially, to check version compatibility.Short answer: No. Here's a quick demonstration:
class Config implements Serializable {
static String region = "ap-south-1";
String name = "orders";
}
// serialize a Config, then set Config.region = "us-east-1", then deserialize:
// restored.name → "orders" (instance state restored)
// Config.region → "us-east-1" (the CURRENT static value; nothing was restored)
If static state must travel with an object, copy it into an instance field, or handle it in custom writeObject/readObject hooks.
Short answer: writeObject aborts, and the exception propagates to the caller:
NotSerializableException for a non-serializable object in the graph;InvalidClassException for class problems;IOException for stream or disk failures;writeObject throws.The stream is left partially written and unusable. Treat the output as corrupt, and don't try to resume.
Key points to cover:
ObjectOutputStream isn't transactional. Serialize into memory first (ByteArrayOutputStream) if you need all-or-nothing behaviour.Serializable class has a member that isn't serializable. What happens, and how do you fix it?Short answer: Serialization throws java.io.NotSerializableException, naming the offending class. The fixes are:
Serializable, if you own it.transient (and rebuild it after deserialization, lazily or in readObject).writeObject/readObject to save a serializable representation (for example, save a connection's URL instead of the connection itself).writeReplace/readResolve).public class ReportJob implements Serializable {
private final String reportId;
private transient DataSource dataSource; // not serializable: re-acquired after load
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.dataSource = DataSourceRegistry.lookup("reports");
}
}
Key points to cover:
transient fields come back as null, 0 or false. Every piece of code that uses them must cope with that, or rebuild them.NoClassDefFoundError and ClassNotFoundException?Short answer:
ClassNotFoundException | NoClassDefFoundError | |
|---|---|---|
| Type | Checked exception | Error (a LinkageError) |
| When | Explicit dynamic loading by name (Class.forName, ClassLoader.loadClass) can't find the class | The JVM implicitly needs a class that was present when compiling, but it can't load or define it now |
| Typical causes | Wrong class name or plugin name, missing driver JAR, a class-loader mismatch | A JAR missing at runtime (the wrong scope, such as provided), version conflicts, or a class whose static initialisation previously failed |
Common trap: saying "ClassNotFoundException means the class existed at compile time". Dynamic loading by name involves no compile-time reference at all. That describes NoClassDefFoundError.
Short answer: The first attempt to initialise the class throws ExceptionInInitializerError, wrapping the cause. The class is then marked as erroneous. Every later use throws NoClassDefFoundError ("Could not initialize class X"), with no stack trace of the original cause in older JDKs (newer JDKs link the original exception).
Key points to cover:
NoClassDefFoundErrors are just echoes.Short answer: When the failure is expected, recoverable, and the caller can reasonably do something about it, so the compiler should force the caller to decide.
ImportFormatException, so the caller can show the user which row is wrong.PaymentDeclinedException, so the caller must choose between retrying with another card and cancelling the order.Use unchecked exceptions for programming errors and violated preconditions (IllegalArgumentException, IllegalStateException), and for failures the immediate caller can't handle.
Key points to cover:
finally block? Describe a scenario.Short answer: Typical real uses are guaranteed cleanup that isn't an AutoCloseable:
ReentrantLock (lock.unlock() in finally);MDC.clear(), threadLocal.remove());For resources that implement AutoCloseable, use try-with-resources instead.
MDC.put("orderId", orderId);
try {
processOrder(orderId);
} finally {
MDC.remove("orderId"); // otherwise the next task on this pooled thread logs the wrong orderId
}
finally block ever caused unexpected behaviour?Short answer: The common surprises:
finally masks the original exception from try. For example, close() fails and hides the real error. try-with-resources fixes this by attaching close failures as suppressed exceptions.return in finally swallows exceptions and overrides the return value.finally delays error propagation.finally runs even on the error path, where objects may be partially initialised, which can cause NPEs.try (var in = Files.newInputStream(path)) {
parse(in);
} catch (IOException e) {
for (Throwable s : e.getSuppressed()) log.warn("close failed too", s); // not lost
throw e;
}
Throwable bad practice?Short answer: Throwable includes Errors: OutOfMemoryError, StackOverflowError and LinkageError. After one of those, the JVM or application may be in an inconsistent state. Catching and continuing hides fatal conditions, can corrupt data, and keeps a sick instance serving traffic instead of letting it crash and restart. It also catches ThreadDeath, and interferes with frameworks that rely on errors propagating.
Key points to cover:
Q: What are suppressed exceptions?
A: When an exception is already propagating and another one occurs during cleanup (such as close() in try-with-resources), the second is attached to the first with addSuppressed, rather than replacing it. Retrieve them with getSuppressed().
Q: Should you log and rethrow an exception? A: Usually not both at every layer, because you'd log the same error many times. Log once, at the boundary that handles it (a controller advice, or a message-listener error handler), and wrap with context when rethrowing.
Q: How do you design a custom exception hierarchy?
A: A small base exception per module (OrderException extends RuntimeException), with specific subclasses for the cases callers handle differently. Include context fields (order ID, error code), and always keep the cause.
Q: Does Java 21 change exception handling? A: Not fundamentally. Structured concurrency (preview in 21) propagates subtask failures to the parent scope, and sealed result types plus pattern matching make "error as a value" styles more practical alongside exceptions.