Exceptions, try, catch and finally
Things go wrong. A file is missing, a number will not parse, a network call times out, a customer types their name into the tiffin-count box. Exceptions are how Java reports that, and how you decide what to do about it.
The mechanism is straightforward. The judgement — what to catch, what to let through, what to say — is the part worth a module, and this lesson starts with the mechanism because you cannot exercise judgement without it.
What an exception actually is
An object, thrown up the call stack until something catches it. If nothing does, the thread dies and the JVM prints a stack trace.
throw new IllegalArgumentException("tiffins must be between 0 and 62, got " + n);
throw is a statement, like return. Everything after it in that block is
unreachable, and the compiler will tell you so.
try and catch
try {
int tiffins = Integer.parseInt(input);
System.out.println(tiffins);
} catch (NumberFormatException e) {
System.out.println("[" + input + "] is not a whole number");
}
The try block is the risky part. The catch block runs only if that exception
type is thrown, and execution continues after the whole construct.
Catch the narrowest type you can handle. catch (Exception e) catches
everything including bugs you would rather see, and it is the most common piece
of bad exception code in existence.
Several catches
try {
...
} catch (NumberFormatException e) {
...
} catch (IOException e) {
...
}
Order matters: most specific first. A catch (Exception e) above a
catch (NumberFormatException e) makes the second unreachable, and the compiler
refuses to build it — one of the more helpful errors in the language.
When two types get the same treatment, use multi-catch:
try {
return Integer.parseInt(input);
} catch (NumberFormatException | NullPointerException e) {
return 0;
}
NumberFormatException: For input string: "abc"
NumberFormatException: Cannot parse null string
parsed 42
Worth noticing in that output: Integer.parseInt(null) throws
NumberFormatException, not NullPointerException — a detail most people
guess wrong.
finally
Runs whether or not an exception was thrown, and whether or not one was caught.
try {
sb.append("try ");
throw new IllegalStateException("boom");
} catch (IllegalStateException e) {
sb.append("catch ");
return sb.append("returned").toString();
} finally {
System.out.println("finally ran before the method returned");
}
finally ran before the method returned
try catch returned
The finally block runs after the return value has been computed but before
the method actually returns. That ordering surprises people and is worth seeing
once.
The finally that eats your exception
static String swallowed() {
try {
throw new IllegalStateException("this exception disappears");
} finally {
return "the finally block's return wins";
}
}
the finally block's return wins
The exception vanished. No trace, no log, nothing. A return — or a throw —
inside finally discards whatever was in flight.
Never return or throw from a finally block. javac -Xlint warns about
it, IntelliJ warns about it, and it still reaches production because somebody
added a return to tidy up.
The legitimate use of finally is releasing something, and even that is better
done with try-with-resources — which is the lesson four along.
Wrapping, and the cause chain
When you catch something and cannot handle it here, rethrow with context:
static void loadConfig() {
try {
Integer.parseInt(raw);
} catch (NumberFormatException e) {
throw new IllegalStateException("could not read config value 'port'", e);
}
}
java.lang.IllegalStateException: could not read config value 'port'
caused by: java.lang.NumberFormatException: For input string: "not a number"
The second constructor argument is the cause, and it preserves the original exception and its stack trace. Without it, you have replaced a precise technical error with a vague one and thrown away where it happened.
Always pass the cause when wrapping. The next lesson shows what the cause chain looks like in a real trace and why it is the most useful part.
The hierarchy
Throwable
├── Error — do not catch. OutOfMemoryError, StackOverflowError
└── Exception
├── RuntimeException — unchecked. Bugs and bad input
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IllegalStateException
│ ├── IndexOutOfBoundsException
│ ├── NumberFormatException
│ └── ArithmeticException
└── everything else — checked. IOException, SQLException
Error means the JVM is in trouble, not your code. Catching
OutOfMemoryError to "handle" it almost never works, because the handler needs
memory too. Leave them alone.
The split between checked and unchecked is the whole of the next lesson but one.
The ones you will meet
| Exception | Usually means |
|---|---|
NullPointerException |
Something was null that should not have been |
IllegalArgumentException |
A caller passed something invalid |
IllegalStateException |
The object is not in a state where this makes sense |
IndexOutOfBoundsException |
An index past the end |
NumberFormatException |
parseInt on something that is not a number |
ArithmeticException |
Integer division by zero |
ClassCastException |
A cast that was not true |
ConcurrentModificationException |
Modified a collection while iterating it |
UnsupportedOperationException |
Modified an immutable collection |
NoSuchElementException |
Optional.get() on empty, or an exhausted iterator |
IOException |
A file or network operation failed — checked |
Every one of those has appeared in an earlier module as a real failure. That was deliberate.
What not to do
Do not swallow.
catch (Exception e) {
// ignore
}
The problem still happened; you have only removed the evidence. If an exception is genuinely expected and harmless, say so in a comment explaining why — the comment is the point.
Do not catch Exception to be safe. It catches NullPointerException too,
which is a bug you wanted to hear about.
Do not use exceptions for control flow.
try {
return list.get(i);
} catch (IndexOutOfBoundsException e) {
return null;
}
Check i < list.size(). Exceptions are expensive to construct — filling in the
stack trace is the costly part — and this hides the intent.
Do not log and rethrow. You get the same failure in the log twice, from two places, and neither is the whole picture. Either handle it or let it go up.
Do not throw Exception or RuntimeException directly. They tell a caller
nothing. Throw something specific, or write your own — lesson four.
Check your work
What is the difference between Error and Exception? Error means the JVM
itself is in trouble — out of memory, stack overflow — and should not be caught.
Exception is for conditions your program can reasonably deal with.
In what order must catch blocks appear? Most specific first. A broader type
above a narrower one makes the narrower unreachable and fails to compile.
When does finally run? Always — after the try and any catch, and after
the return value is computed but before the method returns.
What happens if you return from finally? Any in-flight exception is
discarded silently. Never do it.
What does Integer.parseInt(null) throw? NumberFormatException: Cannot parse null string, not NullPointerException.
Why pass the cause when wrapping? It keeps the original exception and its stack trace. Without it you have replaced a precise error with a vague one and lost where it happened.
Name three things not to do. Swallow silently; catch Exception broadly; use
exceptions for ordinary control flow; log and rethrow; throw bare Exception.
Practice 3, the finally ordering. The output is finally ran before the method returned and then try catch returned. The finally block executes
after the return value has been built and before control leaves the method.
Practice 5, the swallowed exception. The method returns the finally block's return wins and the IllegalStateException disappears completely — no stack
trace, no log, nothing to search for. The fix is to move the return out of the
finally, and the general rule is that finally should only release resources.
Practice
-
Catch a parse failure. Read a line, parse it, and print a message naming the bad input when it fails. Then feed it
abc, an empty line and42. -
Cause the top six. Deliberately trigger
NullPointerException,ArithmeticException(integer division by zero),ArrayIndexOutOfBoundsException,NumberFormatException,ClassCastExceptionandUnsupportedOperationException. Read each message. -
Prove the
finallyordering. Write the method above with a print in each block and areturnin thecatch. Write down the order before running it. -
Break the catch order. Put
catch (Exception e)beforecatch (NumberFormatException e)and read the compile error. -
Swallow an exception, then stop. Write the
finallywith areturn, confirm the exception vanishes, then fix it. Then turn onjavac -Xlint:finallyand confirm the compiler was willing to warn you all along. -
Harder — wrap with context. Write a three-level call chain where the deepest level parses a number. Make the middle level catch the
NumberFormatExceptionand rethrow anIllegalStateExceptionnaming the row and column, with the original as the cause. Printe.getCause()at the top. Then remove the cause argument and note exactly what information you lost.
Next: reading a stack trace, which is where all of this actually reaches you.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship