How Hirenix teaches
One chapter. 90 minutes.
Interview-ready.
Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.
- 📖Concept in 5 minutesNo jargon — straight to the point
- 🛠️Real-world problemThe kind production code throws at you
- 💬Model answerExactly what to say in the room
- 🧠FlashcardsRevise in 10 minutes
- 🤖AI mock interviewIt asks follow-ups too
- 📊Weak topicsSee exactly where you're stuck

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.
What you’ll learn
- ●Exceptions and the Throwable hierarchy
- ●Checked vs unchecked exceptions
- try, catch, finallyFree account
- throw vs throws, and propagationFree account
- Multi-catch and catch orderFree account
- try-with-resources and suppressed exceptionsFree account
- Custom exceptions and chainingFree account
- Reading stack traces and modern NPE messagesFree account
- Exception antipatterns and best practicesFree account
- Overriding and the throws clauseFree account
- Files and paths: java.io vs java.nio.fileFree account
- Reading and writing text filesFree account
- Streams, buffering and character encodingFree account
- Serialization basicsFree account
- RecapFree account
- ●Project: CSV Report Loader
- Project: Log File AnalyzerFree account
- Project: Notes File ManagerFree account
Exceptions and the Throwable hierarchy
Picture a railway reservation counter. One clerk, one queue, and a fixed set of steps: take the form, check the train, print the ticket, hand it over.
Three things go wrong on the same morning.
A passenger asks for a train that does not exist. The clerk stops, says "no such train", calls the next person. Work continued.
The printer runs out of paper. The clerk stops, refills it, prints again. Slower, but the counter is still open.
Then the station loses power and the backup is dead. The clerk can do nothing at all. Refilling paper does not help. Being careful does not help. The counter closes.
The first two are situations the clerk is equipped to deal with. The third is a failure of the ground he is standing on. Java draws exactly this line, and every other topic in this chapter is a consequence of it.
In production the "no such train" and "out of paper" cases are exceptions — a bad date string, a missing file, a null where you expected an object. Your code can catch them and do something sensible: ask again, use a default, log the bad row and move to the next one. The dead power supply is an Error — the JVM's own heap is exhausted or its stack is full. There is no sensible recovery left, because the machinery that would run your recovery code is itself the broken thing.
The mechanism is one sentence. When a method hits a problem it throws an object. The JVM then walks back up the call stack, frame by frame, looking for the nearest catch whose type matches that object. The moment it finds one, control jumps there and the program carries on. If it never finds one, the JVM prints a stack trace and kills the thread — which is the "Exception in thread main..." crash you have already seen.
🌍 Real-world example: a payments service reads a CSV of 5,000 refunds at night. Row 4,182 has the amount written as
1,200instead of1200. That row throws aNumberFormatException. Handled, the job skips one row and 4,999 refunds go out; unhandled, the whole thread dies and every remaining customer waits until morning. Same defect, two very different Monday mornings.
💡 Exception = an event that interrupts the normal flow of a program at runtime, represented as an object. 💡
Throwable= the root class of the whole family, injava.lang. It is the only type Java lets you put afterthrowor inside acatch.throw "oops";does not even compile — javac saysincompatible types: String cannot be converted to Throwable. 💡Error= a direct subclass ofThrowablefor failures of the JVM or the environment. You are not meant to catch these. 💡Exception= the other direct subclass ofThrowable, for conditions an application can reasonably be expected to handle. 💡RuntimeException= a subclass ofExceptionwhose descendants the compiler does not police. These are the unchecked ones —NullPointerException,ArithmeticException,IllegalArgumentException. 💡 Stack unwinding = the JVM discarding call frames one by one while it searches for a matchingcatch.
The map — draw this, then everything else follows
Object
|
Throwable <- the ONLY type you can throw or catch
/ \
Error Exception "the app can reasonably handle this"
| / \
VirtualMachineError / RuntimeException
/ \ / / | \
StackOverflowError OutOfMemoryError NullPointer Arithmetic IllegalArgument
| Exception Exception Exception
IOException, SQLException,
ClassNotFoundException, ... UNCHECKED
CHECKED (compiler forces you) (compiler stays quiet)
Four things in that picture are worth saying out loud, because they are what interviewers actually poke at.
Throwable is the gate. Nothing outside this tree can be thrown or caught. That is why the family tree matters at all: catch matches by type, so where a class sits decides what catches it. catch (Exception e) catches every branch under Exception — and nothing under Error.
Error and Exception are siblings, not parent and child. An Error is not an Exception. Run the demo below and you will see false — and you will also learn that writing the obvious version of that check does not compile at all.
RuntimeException sits under Exception. So every RuntimeException is an Exception, which is why catch (Exception e) quietly swallows NullPointerException too. The checked/unchecked split is not a split in the tree, it is a rule the compiler applies to one branch of it. Topic 2 is entirely about that rule.
Only Throwable carries a stack trace. That is what getMessage(), getCause() and printStackTrace() hang off, and why you cannot invent your own error type from scratch — you extend something in this tree.
Error vs Exception — which is worth recovering from
Exception |
Error |
|
|---|---|---|
| Caused by | your code or the world outside it | the JVM or the environment itself |
| Examples | IOException, NumberFormatException, NullPointerException |
StackOverflowError, OutOfMemoryError, NoClassDefFoundError |
| Recover? | often yes — retry, default, skip | no meaningful recovery |
| Compiler involved? | yes, for the checked branch | never |
| Your move | catch it where you can act, otherwise let it rise | let it kill the process, then fix the cause |
The honest test is not "how bad does it sound". It is: is there an action my program can take that makes this better? A missing config file has one — fall back to a default, or fail with a message a human can act on. A full heap does not; allocating a log message may itself fail.
ClassNotFoundException vs NoClassDefFoundError
This pair is on both major interview lists, and it is the cleanest real example of the Error / Exception split — which is exactly why it lives here and not in a trivia list.
ClassNotFoundException is a checked Exception. It happens when you ask for a class by name, at runtime — Class.forName("com.mysql.cj.jdbc.Driver"), or a reflective lookup, or a class loader — and the loader cannot find it. Your code asked, so your code can answer: catch it, print "MySQL driver JAR is missing from the classpath", exit with a clear message. The demo below does this and the program keeps going.
NoClassDefFoundError is an Error. It means the class was present when your code was compiled but is not loadable now. The JVM was already committed to using it, so this is a broken environment, not a request that failed. The most common real cause is not even a missing jar — it is a class whose static initializer blew up on the first touch. The first access throws ExceptionInInitializerError; every access after that throws NoClassDefFoundError, because the class is now permanently marked unusable:
access 1 -> java.lang.ExceptionInInitializerError
access 2 -> java.lang.NoClassDefFoundError
That is real output from a class whose static final int VALUE = Integer.parseInt("nahi-number"); failed. The one-line answer for an interview: ClassNotFoundException = you asked for it by name and it was not there. NoClassDefFoundError = it was there at compile time and the JVM cannot load it now. One is a request that failed; the other is a broken environment.
StackOverflowError vs OutOfMemoryError
Ch1 already taught the memory model, so this is only the pointer: stack holds call frames and locals, heap holds objects. Two regions, two errors, and both are VirtualMachineError — that superclass name is the tell that the JVM itself gave up.
StackOverflowError— the stack ran out. Almost always recursion with no base case, or a base case that is never reached. Fix the recursion, or rewrite it as a loop.OutOfMemoryError— the heap ran out. Too many live objects, or objects that are never released. On a run with-Xmx32mthis machine printed the messageJava heap space. Fix the leak or the batch size; a bigger-Xmxonly moves the crash later.
Wrong answer to avoid: "both mean the program used too much memory, so increase the memory." A StackOverflowError is nearly always a logic bug, and no heap setting touches it.
An Error can be caught — and you still should not
The popular line is "you cannot catch an Error". That is false, and an interviewer who knows it will use it. Error extends Throwable, so catch (StackOverflowError e) compiles and works — line H of the demo below caught one, and the program printed one more line afterwards.
So why not? Because catching it does not undo anything. After an OutOfMemoryError the heap is still full and your handler's own allocation may throw again. After a StackOverflowError you are running on a stack you just proved is unreliable. You have converted a loud, diagnosable crash into a program that limps on in an unknown state — and the log line you wanted may never get written. Interview answer: yes, technically, because Error extends Throwable — but you should not, because there is nothing left to recover. The only defensible use is a top-level handler that logs and then exits. The same logic makes catch (Throwable t) a bad habit: it catches Error too, by accident.
One compiler detail worth carrying: new OutOfMemoryError() instanceof Exception is not false — it is a compile error, incompatible types. javac can prove the two types can never relate, so it refuses the question instead of answering it. You only get the false after widening to Object, which is what the demo does.
Standard definition: An exception in Java is an event that disrupts the normal flow of a program at runtime, represented by an object inheriting from java.lang.Throwable; Throwable has two direct subclasses, Error for serious problems an application should not try to recover from, and Exception for conditions a well-written application can catch and handle, with RuntimeException being the unchecked branch of Exception that the compiler does not enforce.
When to use it: catch an exception at the level where you can actually do something about it — a loop that reads rows catches the bad-row exception so the other 4,999 rows still process; a controller catches a parse failure and returns "invalid date" to the user. That is a real decision, not a formality.
When NOT to use it: do not catch at a level that can only re-log and rethrow — let it rise to somewhere that can decide. And never catch Error, Throwable, or (as a habit) a bare Exception around a whole method: those hide the failures you most need to see, including the JVM's own. Trade-off: every catch you add buys the program a chance to continue and costs you a signal that something is wrong. If you cannot name what the catch block will do other than print, the honest choice is to let the exception travel up.
public class T1 {
// recursion that never stops - it will fill the stack
static void deep(int n) { deep(n + 1); }
// prints the full parent chain of any Throwable
static String chain(Throwable t) {
StringBuilder sb = new StringBuilder();
for (Class<?> c = t.getClass(); c != null; c = c.getSuperclass()) {
if (sb.length() > 0) sb.append(" -> ");
sb.append(c.getSimpleName());
}
return sb.toString();
}
public static void main(String[] args) {
// 1. the mechanism: throw an object, catch it by type, keep running
try {
int[] marks = new int[3];
marks[5] = 90;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("A caught : " + e);
}
// 2. the map is real - ask the classes themselves
System.out.println("B chain : " + chain(new ArithmeticException("x")));
System.out.println("C chain : " + chain(new java.io.IOException("x")));
System.out.println("D chain : " + chain(new StackOverflowError()));
// 3. an Error is NOT an Exception. Widen to Object first, otherwise
// javac refuses the instanceof as "incompatible types"
Object oome = new OutOfMemoryError("demo");
System.out.println("E OutOfMemoryError instanceof Exception = " + (oome instanceof Exception));
System.out.println("F OutOfMemoryError instanceof Throwable = " + (oome instanceof Throwable));
// 4. ClassNotFoundException - a checked Exception, thrown when YOU ask by name
try {
Class.forName("com.hirenix.KoiAisiClassNahiHai");
} catch (ClassNotFoundException e) {
System.out.println("G " + e);
}
// 5. an Error CAN be caught - and this is exactly what you must not do
try {
deep(1);
} catch (StackOverflowError e) {
System.out.println("H an Error WAS caught: " + e.getClass().getName());
}
System.out.println("I program is still running");
}
}Checked vs unchecked exceptions
Think about walking out of your flat in the morning. Two completely different kinds of trouble can hit you, and the building treats them completely differently.
The first is your own mistake - you left the keys on the table, or you walked into the glass door. Nobody can write a rule for that. There is no form to sign, no checklist at the gate. You just get better at not doing it.
The second is the rain. Rain is not your mistake. It is a fact about the world outside, and it can happen on any day, to anyone, however careful you are. So the building has a rule, and the security guard at the gate enforces it: you may not step out unless you are carrying an umbrella, or you have signed a note saying "I am going out without one, and whoever is waiting for me knows they may have to deal with a soaked me." One or the other. With neither, he simply does not open the gate.
And notice what the guard does not do - he never stops you for forgetting your keys.
In production that guard is javac, the Java compiler. The rain is a checked exception: something outside your program that can fail no matter how good your code is - a missing file, a dead network, a database that went down. The forgotten keys are an unchecked exception: a bug in your own logic - an index past the end of an array, a null you did not expect, a division by zero.
🌍 Real-world example: a payments service reads a settlement file that the bank uploads every night. If the bank's upload job failed, the file is not there. That is not a bug in the payments service - the code is perfect, the world is not. Java forces the developer to say out loud what happens in that case. But if the same service crashes with
ArrayIndexOutOfBoundsExceptionon row 200, no rule would have helped: that is a broken loop and it needs a fix, not paperwork.
💡 Checked exception = an exception the compiler checks up on. If a method can throw one, every caller must either catch it or declare it, or the code will not compile. 💡 Unchecked exception = anything under
RuntimeException(and anything underError). The compiler says nothing about it; handling it is entirely your choice. 💡throws= a clause in the method signature that declares "this method may hand this exception back to you - you deal with it". It handles nothing itself; it passes the problem up. 💡 Compile time = whilejavacis reading your source. Runtime = while the JVM is executing the program. Two different moments, and this whole topic lives on the difference.
The answer that actually wins this question
The popular answer is "checked exceptions happen at compile time, unchecked exceptions happen at runtime." It is wrong, and a good interviewer is listening for exactly that sentence.
Nothing is ever thrown at compile time. The compiler never opens your file, never dials the network, never runs a single loop. FileNotFoundException and NullPointerException are both objects that get created and thrown while the program is running, on a real machine, with real data.
The real difference is a contract the compiler forces on you before it will produce a .class file:
| checked | unchecked | |
|---|---|---|
| Thrown at | runtime | runtime |
Does javac demand anything? |
yes - catch it or declare it | no - nothing at all |
| Where it sits in the tree | under Exception, outside RuntimeException |
under RuntimeException or under Error |
| What it usually means | the outside world failed | your code has a bug |
| Examples | IOException, SQLException, InterruptedException |
NullPointerException, ArrayIndexOutOfBoundsException |
Say it in one line: both are runtime events; the difference is what javac forces you to write in the source.
The map - and the one rule that decides everything
Throwable
|
+-- Error ................................. UNCHECKED (OutOfMemoryError, StackOverflowError)
|
+-- Exception ............................. CHECKED
|
+-- IOException ..................... checked (FileNotFoundException sits inside this)
+-- SQLException .................... checked
+-- ClassNotFoundException .......... checked
|
+-- RuntimeException ................ UNCHECKED
+-- NullPointerException
+-- ArrayIndexOutOfBoundsException
+-- NumberFormatException
+-- ArithmeticException
+-- ClassCastException
There is no keyword and no annotation that marks an exception as checked. It is decided purely by where the class sits in that tree. Everything under RuntimeException is unchecked. Everything under Error is unchecked. Everything else under Exception is checked. That is the entire rule, and it is why NumberFormatException - which sounds exactly like a checked exception - is unchecked: its parent chain is IllegalArgumentException -> RuntimeException.
Unchecked exceptions - the ones you will be asked to list
| Exception | When it fires |
|---|---|
NullPointerException |
calling a method on a reference that is null |
ArrayIndexOutOfBoundsException |
arr[5] on an array of length 3 |
NumberFormatException |
Integer.parseInt("abc") |
ArithmeticException |
integer 1 / 0 |
ClassCastException |
casting a String to an Integer |
IllegalArgumentException / IllegalStateException |
a method was called with bad input, or at the wrong time |
ConcurrentModificationException |
removing from a list while a for-each loop is walking it |
Look at what they have in common: every single one is fixable by changing your code. You do not "handle" ArrayIndexOutOfBoundsException - you fix the loop bound. Wrapping a bug in a try-catch does not stop it being a bug; it only hides it. That is exactly why the compiler stays silent about them: demanding a catch around every possible bug would mean a catch around every line ever written.
One of those examples has a trap hiding inside it. ArithmeticException fires on 1 / 0 - but only for integers. Floating-point division does not throw at all:
1/0 -> java.lang.ArithmeticException: / by zero
1.0/0 -> Infinity
0.0/0 -> NaN
total -> NaN
1.0 / 0 is Infinity, 0.0 / 0 is NaN, and the program simply keeps running. The last line is the damage: 100 + NaN is still NaN, so one bad average quietly poisons a whole report and nobody ever sees a stack trace. That is this chapter's theme in miniature - a crash tells you something went wrong; a silent wrong number does not. Which is also why "dividing by zero always throws" is a wrong answer.
Checked exceptions - and the door into file I/O
| Exception | When it fires |
|---|---|
IOException |
any file / stream / network read or write that fails |
FileNotFoundException |
a subclass of IOException - the path does not exist or cannot be read |
SQLException |
a database call failed |
ClassNotFoundException |
Class.forName("...") could not find the class |
InterruptedException |
a sleeping or waiting thread was interrupted |
ParseException |
SimpleDateFormat could not read the date string |
Every one of them is the program talking to something outside itself - a disk, a socket, a database, another thread.
Here is the sentence that makes the rest of this chapter make sense: a missing file is not a programming mistake, it is a fact about the world. Your code can be flawless and the file can still be gone, because a human deleted it, or a mount dropped, or the upload never finished. No amount of careful coding removes that possibility - so Java refuses to let you ignore it silently. That is the whole reason IOException is checked, and it is why the second half of this chapter, files and streams, is full of checked exceptions.
Exactly two ways to handle a checked exception
This is the one place in Java where "handle it" means two options, not one - and the interviewer usually wants both named.
1. Catch it - you take responsibility here.
try {
return Files.readString(Path.of("config.txt"));
} catch (IOException e) {
return "default-config"; // I know what to do without the file
}
2. Declare it with throws - you refuse responsibility and pass it up.
String loadConfig() throws IOException {
return Files.readString(Path.of("config.txt")); // caller decides
}
There is no third option. You cannot ignore it, and there is no switch that turns the check off. If you write neither, javac stops with unreported exception ... must be caught or declared to be thrown and produces no class file at all.
Note carefully: throws does not handle anything. It is a declaration, not a solution. It moves the same decision one level up the call stack - and if every method keeps declaring it all the way up to main, the JVM ends up printing the stack trace and killing the program.
So which of the two? Ask one question
Can I actually fix this situation here? Yes ->
catchit, and do the fixing thing: use a default, retry, show the user a real message. No -> let it go up withthrows, so someone with more context decides.
A DAO buried deep in your code has no idea whether a missing config file should end the program or fall back to defaults - it should declare and move on. The startup code at the top does know - it should catch, log a readable message, and shut down cleanly. Catching an exception you can do nothing about is how a real failure turns into a silent one.
When to use it: make your own exception unchecked (extend RuntimeException) when it signals a programming bug or a condition nobody up the stack can recover from - bad arguments, a broken invariant, a config value that should have been validated at startup. Nobody gains from being forced to write catch around a bug.
When NOT to use it: make it checked (extend Exception) only when a caller can realistically do something different because of it - retry the download, fall back to a cached file, ask the user for another path. If you cannot name that recovery action, checked is the wrong choice: you are only forcing try-catch noise into every caller.
Trade-off: checked exceptions buy you a compiler-enforced guarantee that nobody forgot the failure case, and they cost you signature churn - adding one throws can ripple through ten methods. That cost is exactly why modern frameworks such as Spring and Hibernate, and most HTTP clients, wrap their checked exceptions in unchecked ones. That is a deliberate trade, not a shortcut: they gave up the compiler's reminder in exchange for clean signatures.
The trap almost every learner gets wrong
Integer.parseInt(null) - what does it throw? Almost everyone answers NullPointerException. It is a NumberFormatException, and the practice section below has the real output. parseInt never dereferences the string; it validates it and rejects it as unparseable. Both of those are unchecked, so the compiler never warned you either way - which is precisely why the only reliable way to know is to run it.
Standard definition: A checked exception is any Throwable that is neither a RuntimeException nor an Error. The compiler requires every checked exception to be either caught in a try-catch block or declared in the method's throws clause; unchecked exceptions carry no such requirement, even though both kinds are thrown at runtime.
import java.io.FileReader;
import java.io.IOException;
public class Demo {
// Way 1 - CATCH: I can handle this situation right here
static String readOrDefault(String path) {
try (FileReader r = new FileReader(path)) {
return "file found";
} catch (IOException e) {
return "default value (" + e.getClass().getSimpleName() + ")";
}
}
// Way 2 - DECLARE: I cannot fix it, let the caller decide
static String readStrict(String path) throws IOException {
try (FileReader r = new FileReader(path)) {
return "file found";
}
}
public static void main(String[] args) {
System.out.println("A caught here = " + readOrDefault("nahi-hai.txt"));
try {
System.out.println("B declared = " + readStrict("nahi-hai.txt"));
} catch (IOException e) {
System.out.println("B declared = caller caught it: " + e.getClass().getSimpleName());
}
// Unchecked - the compiler asked us to write nothing at all
int[] marks = new int[3];
try {
System.out.println(marks[5]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("C unchecked = " + e);
}
}
}Project: CSV Report Loader
A college clerk has a stack of 200 admission forms on his desk and one register to fill in. Form 3 has the marks written as achha instead of a number. Form 5 is missing the marks column altogether. Does he bin the whole stack and go home?
No. He moves those two forms onto a problem pile, enters the other 198, and hands the principal two things: the filled register, and the problem pile with a note on each form saying what was wrong with it.
Now change one thing. He walks in and the register cupboard is locked, and nobody has the key. There is no problem pile for that. He stops, and says so loudly, at the start, to the one person who can find the key.
That is this whole chapter sitting on one desk:
A bad row is recoverable, so you catch it and continue. A missing file is not, so you let it stop you.
In production the stack of forms is a CSV another team exported, and it is never clean. A loader that dies on the first bad row becomes a nightly job that fails at 2 a.m. having imported 3 rows out of 40,000, leaving no list of what was wrong. A loader that survives bad rows imports 39,996 and hands you a report of the four it could not use.
🌍 Real-world example: a college uploads a marks sheet to a results portal. Row 3 has
not-a-numberin the marks column. If the portal crashes, the clerk sees "Upload failed", has no idea which of 400 rows to fix, and re-uploads the same file. If the portal loads what it can, he sees "396 loaded, 4 rejected — line 3: marks is not a number", fixes four rows in two minutes, and re-uploads. Same bad data, completely different day.
💡 Recoverable failure = one where the caller has a genuinely useful alternative action. Skipping one row and importing the other 199 is useful. Pretending an unreadable file was read is not. 💡
InvalidRowException= the custom checked exception you write here. It extendsExceptionand carries the line number and the raw text of the row that failed. 💡 Cause = the original low-level exception (hereNumberFormatException) handed to your exception's constructor, so the report can still show what the parser actually complained about. 💡 Collect-and-report = adding each failure to aListwhile the loop keeps running, and printing them all at the end instead of one interruption at a time.
What you are building
CsvReportLoader — one file, no libraries, that reads marks.csv inside try-with-resources; rejects a row for four separate reasons (blank line, wrong column count, non-numeric field, marks outside 0–100); keeps going after every rejection; prints how many rows loaded, how many were rejected and why; and stops with a clear message if the file does not exist at all.
Step 0 — the data file
Create marks.csv beside your .java file. Type it exactly, broken rows included — the broken rows are the point:
id,name,marks
1,Aarav,88
2,Diya,not-a-number
3,Ishaan,76
4,Neha
5,Kabir,105
6,Meera,64
7,Rohan,abc,extra
8,Zoya,91
Line 6 is genuinely empty; leave it empty. Count lines including the header, because line numbers are what your report will quote and they must match the file a human opens in Excel.
Four good rows and five bad ones: line 3 has text in the marks column, line 5 has two columns, line 6 is blank, line 7 has marks of 105, line 9 has four columns. A file this hostile is unrealistic on purpose, so every branch runs the first time.
Step 1 — read it safely and count lines
try (BufferedReader br = new BufferedReader(new FileReader("marks.csv"))) {
String line;
int lineNumber = 0;
while ((line = br.readLine()) != null) {
lineNumber++;
System.out.println(lineNumber + " : [" + line + "]");
}
} catch (IOException e) {
System.out.println("FATAL: " + e);
}
1 : [id,name,marks]
2 : [1,Aarav,88]
...
6 : []
...
10 : [8,Zoya,91]
Three decisions are already made in those six lines:
- The reader is a resource, so it goes in the parentheses.
brcloses on a normal exit, on areturn, and on an exception — and ifclose()itself fails, your real exception still reaches you with the close failure attached as a suppressed one. A hand-writtenfinally { br.close(); }hands you the close failure and loses the real error. readLine()returnsnullat end of file — it does not throw. That is what makeswhile ((line = br.readLine()) != null)correct rather than a trick.- A blank line arrives as
"", notnull. Line 6 printed[]. Never use "empty string" as your end-of-file test.
Step 2 — an exception that carries evidence
A bad row must produce more than the word "invalid". Whoever fixes the file needs which line and what was on it:
static class InvalidRowException extends Exception {
private final int lineNumber;
private final String rawLine;
InvalidRowException(String message, int lineNumber, String rawLine) {
this(message, lineNumber, rawLine, null);
}
InvalidRowException(String message, int lineNumber, String rawLine, Throwable cause) {
super(message, cause);
this.lineNumber = lineNumber;
this.rawLine = rawLine;
}
int getLineNumber() { return lineNumber; }
String getRawLine() { return rawLine; }
}
extends Exceptionmakes it checked, and that is right here. The compiler refuses every call until someone catches it or declaresthrows— which is what you want, because the caller genuinely can recover.- Two fields, because the handler acts on them. Digging a line number back out of
getMessage()with string parsing breaks the day somebody rewords the message. - Two constructors, the second only to accept a
cause. Some rejections have an underlying exception, some do not, so the short one delegates withnull.
Step 3 — parseRow, and the four ways a row is wrong
static Student parseRow(String line, int lineNumber) throws InvalidRowException {
if (line.trim().isEmpty()) {
throw new InvalidRowException("blank line", lineNumber, line);
}
String[] parts = line.split(",");
if (parts.length != 3) {
throw new InvalidRowException("expected 3 columns, found " + parts.length, lineNumber, line);
}
int id;
int marks;
try {
id = Integer.parseInt(parts[0].trim());
marks = Integer.parseInt(parts[2].trim());
} catch (NumberFormatException e) {
throw new InvalidRowException("id and marks must be numbers", lineNumber, line, e);
}
if (marks < 0 || marks > 100) {
throw new InvalidRowException("marks out of range: " + marks, lineNumber, line);
}
return new Student(id, parts[1].trim(), marks);
}
Fed four sample rows, it prints:
OK -> 1 Aarav 88
REJECTED -> line 2: id and marks must be numbers
raw = [2,Diya,not-a-number]
cause = java.lang.NumberFormatException: For input string: "not-a-number"
REJECTED -> line 3: expected 3 columns, found 2
raw = [4,Neha]
cause = null
REJECTED -> line 4: marks out of range: 105
raw = [5,Kabir,105]
cause = null
The first rejection has a cause and the other two have null, and that difference is honest rather than sloppy. NumberFormatException really was thrown by Integer.parseInt, and its message names the exact text that failed — something your own message does not. That one , e keeps it; drop the comma and getCause() returns null forever, because the original object was caught, ignored and garbage collected. The other two rejections were your rule, not a library's, so there was nothing underneath to attach.
Two details. parts[0].trim() is not decoration: Integer.parseInt("12 ") throws on a trailing space, so a space after a comma would reject a perfectly fine row. And marks < 0 || marks > 100 is a business rule, not a parsing one — 105 parses fine, it just is not a possible mark. Real loaders have both kinds, and both belong in the same rejection path.
Step 4 — the try goes INSIDE the loop
This step decides whether the project works, and it is one indentation level.
Leave the inner try out — let InvalidRowException escape the loop — and the entire run is this:
Exception in thread "main" CsvReportLoader$InvalidRowException: id and marks must be numbers
at CsvReportLoader.parseRow(CsvReportLoader.java:51)
at CsvReportLoader.load(CsvReportLoader.java:71)
at CsvReportLoader.main(CsvReportLoader.java:82)
Caused by: java.lang.NumberFormatException: For input string: "not-a-number"
at java.base/java.lang.Integer.parseInt(Integer.java:668)
at CsvReportLoader.parseRow(CsvReportLoader.java:49)
... 2 more
(Line numbers are from my file, and I trimmed two JDK frames.) Note what is not there: no report at all. One bad row on line 3 wiped out four good rows and four other failure reports nobody will ever see. The trace does show the chaining from Step 3 paying off — the top exception is your layer's name for the failure, and the real fault sits in the last Caused by: block.
Now put the try around the single parseRow call, so the catch sits inside the while:
try {
Student s = parseRow(line, lineNumber);
loaded++;
System.out.println("kept line " + lineNumber + " -> " + s);
} catch (InvalidRowException e) {
rejected.add("line " + e.getLineNumber() + ": " + e.getMessage());
System.out.println("skipped line " + lineNumber + " -> " + e.getMessage());
}
kept line 2 -> 1 Aarav 88
skipped line 3 -> id and marks must be numbers
kept line 4 -> 3 Ishaan 76
skipped line 5 -> expected 3 columns, found 2
skipped line 6 -> blank line
skipped line 7 -> marks out of range: 105
kept line 8 -> 6 Meera 64
skipped line 9 -> expected 3 columns, found 4
kept line 10 -> 8 Zoya 91
loaded=4 rejected=5
A catch ends the try block it belongs to, and nothing wider. The try is one statement wide, so catching ends that statement and the while moves on. That is the whole mechanism behind skip-and-continue — no continue keyword, no flag.
Prove it by moving the same try outward to wrap the whole while. Same file, same parser, one indentation level different, and the report collapses to rows loaded = 1 and rows rejected = 1 — lines 4 to 10 are never even read. The width of a try block is a design decision, not formatting.
Step 5 — the summary
load returns the good rows and fills a rejected list handed in by the caller; main prints both halves. Look at its signature: throws IOException. InvalidRowException is handled inside, IOException deliberately is not. The full program and its real output are in the code panel below; the two lines the project exists for are rows loaded = 4 and rows rejected = 5, each rejection then naming the line number, the reason, the cause where there is one, and the raw text. That is a report a non-programmer can act on.
Step 6 — the failure you refuse to recover from
Run the same program against a file that does not exist:
FATAL: cannot read nahi-hai.csv
java.io.FileNotFoundException: nahi-hai.csv (The system cannot find the file specified)
No report, no rows, no average — and that is correct. Printing "0 rows loaded" here would be actively harmful, because "the file has no valid rows" and "there is no file" need completely different fixes from whoever reads the output.
Two footnotes. The text in brackets comes from the operating system, not Java: on this Windows machine it reads (The system cannot find the file specified), on Linux the same exception prints (No such file or directory) — never assert on it in a test. And the exception name depends on the API: new FileReader(...) throws java.io.FileNotFoundException, while Files.newBufferedReader(Path.of("marks.csv")) throws java.nio.file.NoSuchFileException for the same missing file. Catching IOException covers both, which is one good reason to catch the parent here.
Catch and continue when the caller has a real alternative that leaves the program more useful than stopping: one bad row out of many, one malformed record in a batch. The test is not "can I write a catch block" — you always can. It is "is there something sensible to do next?" Let it stop you when there is not: a missing input file, absent startup config, a broken invariant. Catching those and carrying on produces a program that reports success having done nothing, which is worse than a crash, because a crash at least tells somebody. And when you stop, stop clearly — an empty catch block is the opposite of a FATAL line somebody can act on at 2 a.m.
One honest limitation
String.split(",") is not a CSV parser and this lesson does not pretend otherwise. Real CSV allows quoted fields containing commas, and a trailing empty column simply disappears:
[5,Kabir,] -> 2 parts
[1,"Kumar, Aarav",88] -> 4 parts
This loader calls both "wrong column count" — a wrong reason for a row that is arguably fine. For real files use a CSV library (OpenCSV, Apache Commons CSV). The exception handling you built does not change; only the splitting does.
Try it yourself
- Add a duplicate id rule with a
Set<Integer>of ids already seen, rejecting the second occurrence through the sameInvalidRowException. The exception class needs no change at all — that is what makes a custom exception worth having. - Make the program exit non-zero when anything was rejected, so a shell script can spot a partial import. Remember
System.exitdoes not runfinallyblocks. - Add a rejection limit: past 20 bad rows, stop and throw, on the grounds that this is probably the wrong file entirely. That is the interesting middle case — recoverable one at a time, unrecoverable in bulk.
- Swap
new FileReader(fileName)forFiles.newBufferedReader(Path.of(fileName))and rerun the missing-file case. The exception name becomesNoSuchFileExceptionand yourcatch (IOException e)still catches it.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CsvReportLoader {
// CHECKED: a bad row is something the caller can genuinely act on - skip it and carry on.
static class InvalidRowException extends Exception {
private final int lineNumber;
private final String rawLine;
InvalidRowException(String message, int lineNumber, String rawLine) {
this(message, lineNumber, rawLine, null);
}
InvalidRowException(String message, int lineNumber, String rawLine, Throwable cause) {
super(message, cause);
this.lineNumber = lineNumber;
this.rawLine = rawLine;
}
int getLineNumber() { return lineNumber; }
String getRawLine() { return rawLine; }
}
static class Student {
final int id;
final String name;
final int marks;
Student(int id, String name, int marks) {
this.id = id;
this.name = name;
this.marks = marks;
}
@Override public String toString() { return id + " " + name + " " + marks; }
}
static Student parseRow(String line, int lineNumber) throws InvalidRowException {
if (line.trim().isEmpty()) {
throw new InvalidRowException("blank line", lineNumber, line);
}
String[] parts = line.split(",");
if (parts.length != 3) {
throw new InvalidRowException("expected 3 columns, found " + parts.length, lineNumber, line);
}
int id;
int marks;
try {
id = Integer.parseInt(parts[0].trim());
marks = Integer.parseInt(parts[2].trim());
} catch (NumberFormatException e) {
throw new InvalidRowException("id and marks must be numbers", lineNumber, line, e);
}
if (marks < 0 || marks > 100) {
throw new InvalidRowException("marks out of range: " + marks, lineNumber, line);
}
return new Student(id, parts[1].trim(), marks);
}
// Recoverable failures land in rejected. An unreadable file is NOT recoverable here,
// so IOException is declared, not caught - it must reach main and stop the program.
static List<Student> load(String fileName, List<String> rejected) throws IOException {
List<Student> loaded = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String line;
int lineNumber = 0;
while ((line = br.readLine()) != null) {
lineNumber++;
if (lineNumber == 1) {
continue; // header row
}
try {
loaded.add(parseRow(line, lineNumber));
} catch (InvalidRowException e) {
String reason = "line " + e.getLineNumber() + ": " + e.getMessage();
if (e.getCause() != null) {
reason = reason + " (cause: " + e.getCause() + ")";
}
rejected.add(reason + " raw=[" + e.getRawLine() + "]");
}
}
}
return loaded;
}
public static void main(String[] args) {
String fileName = args.length > 0 ? args[0] : "marks.csv";
List<String> rejected = new ArrayList<>();
List<Student> loaded;
try {
loaded = load(fileName, rejected);
} catch (IOException e) {
System.out.println("FATAL: cannot read " + fileName);
System.out.println(" " + e);
return;
}
int total = 0;
System.out.println("== REPORT: " + fileName + " ==");
for (Student s : loaded) {
System.out.println(" " + s);
total += s.marks;
}
System.out.println("rows loaded = " + loaded.size());
System.out.println("rows rejected = " + rejected.size());
for (String r : rejected) {
System.out.println(" " + r);
}
if (!loaded.isEmpty()) {
System.out.println("average marks = " + (total / (double) loaded.size()));
}
}
}Exceptions and File I/Ointerview questions & answers
10 sample questions below — 203+ in the full bank inside.
Show me how you would actually write a custom exception class.
Three things and no more: extend Exception or RuntimeException, provide a (String message) constructor and a (String message, Throwable cause) constructor that each just call super(...), and add a field only if a handler needs to act on it. An empty body is completely legal and most custom exceptions add no behaviour at all. Name the class so it ends in Exception -- that is convention, not a compiler rule, but every reviewer expects it.
In simple terms: It is smaller than people expect -- closer to filling in a form than writing a class. For example: class DataAccessException extends RuntimeException { DataAccessException(String message) { super(message); } DataAccessException(String message, Throwable cause) { super(message, cause); } }. That is the entire class. A field like rowNumber earns its place only because the caller counts skipped rows; without a handler that uses it, it is just extra code.
Which constructors should a custom exception provide, and why those?
At minimum (String message) and (String message, Throwable cause), each doing nothing but calling super(...). The second one is not a nicety -- it is the only normal way a cause ever gets stored, so a class that ships only the message constructor quietly makes chaining impossible for every future caller. There is an older route, initCause(e), but it is a leftover from before Java 1.4 gave Throwable a cause-taking constructor.
In simple terms: Think of the two constructors as two slots on a delivery form: one for what you want to tell the customer, one for the original damaged-goods slip you staple behind it. If the form has no second slot, nobody can ever staple anything, no matter how much they want to. A run makes it concrete: a class declaring only OnlyMessage(String message) and then a call new OnlyMessage("user load failed", e) does not even compile -- javac says the constructor cannot be applied to given types, required: String, found: String,NumberFormatException.
What is a runtime exception? Give me two examples.
A runtime exception is any exception that sits under java.lang.RuntimeException, which makes it unchecked: the compiler demands nothing from you, no catch and no throws. Two everyday examples are NullPointerException, thrown when you call a method on a reference that is null, and ArrayIndexOutOfBoundsException, thrown by marks[5] on an array of length 3. Both almost always mean your own code has a bug, so the fix is to correct the code, not to wrap it in a try-catch.
In simple terms: Think of leaving your keys on the table when you go out. That is your own mistake, nobody can write a building rule for it, and the guard at the gate never stops you over it -- you just get better at not doing it. Concretely, int[] marks = new int[3]; System.out.println(marks[5]); compiled without a single word from javac and only failed when it ran: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3.
What is a custom exception, and why would you write one?
A custom exception is a class you write yourself that extends Exception or RuntimeException, so a failure carries your application's name instead of a library's. You write one when a caller has to tell this failure apart from other failures -- an import that must count bad rows separately from a missing file, or a payment layer that must distinguish a declined card from a gateway timeout. Extending Exception makes it checked, extending RuntimeException makes it unchecked, and that single word is the whole decision.
In simple terms: Think of a hospital lab. The technician finds a cracked tube and can shout 'tube cracked!' down the corridor -- true, and useless to the receptionist standing in front of the patient. What reception needs is 'the test for patient 42 could not be done'. Same event, two vocabularies: one describes the plumbing, one describes the business. Concretely, Integer.parseInt throws NumberFormatException, which says nothing about candidates or rows; the import service catches it and throws its own InvalidRowException("row 47 is broken", 47, e), which the screen can actually show.
What actually decides whether an exception is checked or unchecked -- is there a keyword for it?
There is no keyword and no annotation. It is decided purely by where the class sits in the Throwable tree. Everything under RuntimeException is unchecked, everything under Error is unchecked, and everything else under Exception is checked. That is the entire rule, which is why you can answer this question about any exception just by walking its parent chain.
In simple terms: It is like which floor a flat is on -- nothing is written on the door, the address alone tells you. Concretely, NumberFormatException sounds exactly like a checked exception, but its chain is NumberFormatException -> IllegalArgumentException -> RuntimeException, so it is unchecked. Running new NumberFormatException("x") instanceof RuntimeException on JDK 17 printed true.
Is an Error checked or unchecked?
Unchecked. OutOfMemoryError and StackOverflowError sit under Error, and the compiler demands nothing about them -- no catch, no throws. This is worth saying carefully in an interview: unchecked does not mean the same thing as under RuntimeException. Error is the second unchecked branch, and it represents JVM-level failures you are not expected to recover from.
In simple terms: Forgetting your keys is your own mistake and rain is the world's, but a fire in the building is a third thing entirely -- no rule at the gate helps, you just get out. Concretely, in the tree Throwable splits into Error (unchecked) and Exception, and only the part of Exception outside RuntimeException is checked. That is why you never write catch (OutOfMemoryError e) and javac never asks you to.
Does throws actually handle the exception?
No. throws is a declaration in the method signature, not a solution. It tells the compiler and the caller that this method may hand this exception back to you and you deal with it. Nothing is caught, nothing is recovered -- the same decision simply moves one level up the call stack. And if every method keeps declaring it all the way up to main, the JVM prints the stack trace on System.err and kills the program.
In simple terms: Signing the note at the gate does not keep you dry. It only means somebody upstairs has agreed to deal with a soaked you. Concretely, static String readStrict(String path) throws IOException { ... } compiled with no catch anywhere in its body, and the compiler then held the call site responsible instead -- that call had to sit in its own try-catch, and the run printed B declared = caller caught it: FileNotFoundException.
Give me two examples of checked exceptions.
IOException, and its subclass FileNotFoundException, are the ones you meet first; SQLException is the other classic. ClassNotFoundException, InterruptedException and ParseException are checked too. The definition is that a checked exception is any Throwable that is neither a RuntimeException nor an Error, so every caller must either catch it or declare it. What all of them share is that the program is talking to something outside itself -- a disk, a database, a socket, another thread.
In simple terms: Rain is not your mistake. It is a fact about the world outside, it can happen on any day however careful you are, so the building has a rule about it: umbrella, or a signed note, one of the two. Concretely, writing new FileReader("nahi-hai.txt") with no handling stopped javac dead: unreported exception FileNotFoundException; must be caught or declared to be thrown.
How do you handle a checked exception, and how many ways are there?
Exactly two. Either catch it in a try-catch block and take responsibility right there, or declare it on the method signature with throws and let the caller decide. There is no third option, no way to ignore it and no switch that turns the check off -- write neither and javac stops with unreported exception ... must be caught or declared to be thrown, and produces no class file at all. And note that throws handles nothing; it only moves the same decision one level up the call stack.
In simple terms: The guard at the gate accepts exactly two things: you are carrying an umbrella, or you have signed a note saying whoever is waiting for you knows you may arrive soaked. With neither, he simply does not open the gate. In code that is try { return Files.readString(Path.of("config.txt")); } catch (IOException e) { return "default-config"; } for the first way, and String loadConfig() throws IOException { ... } for the second.
I wrote class NoCtor extends RuntimeException { } and then throw new NoCtor("user load failed"). Does it compile?
No. Constructors are not inherited, so a class with an empty body has only the implicit no-argument constructor, and passing a message to it is a compile error. javac reports: constructor NoCtor in class NoCtor cannot be applied to given types; required: no arguments, found: String. An empty body is legal, but the moment you want a message you must declare the constructor that takes one and pass it up with super(message).
In simple terms: Inheriting a class is like inheriting a shop -- you get the shelves and the tills, but not the previous owner's order forms. Throwable's (String) form is one of those order forms: it exists upstairs, but your class does not automatically offer it to callers. Write NoCtor(String message) { super(message); } and the same throw compiles.
193+ more Exceptions and File I/O questions inside
Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.
Unlock all questions — freeReady to practise Exceptions and File I/O?
Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.