RizTech Academy logo
RizTech Academy
Exceptions and Error HandlingLesson 1 of 625 min

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

  1. 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 and 42.

  2. Cause the top six. Deliberately trigger NullPointerException, ArithmeticException (integer division by zero), ArrayIndexOutOfBoundsException, NumberFormatException, ClassCastException and UnsupportedOperationException. Read each message.

  3. Prove the finally ordering. Write the method above with a print in each block and a return in the catch. Write down the order before running it.

  4. Break the catch order. Put catch (Exception e) before catch (NumberFormatException e) and read the compile error.

  5. Swallow an exception, then stop. Write the finally with a return, confirm the exception vanishes, then fix it. Then turn on javac -Xlint:finally and confirm the compiler was willing to warn you all along.

  6. Harder — wrap with context. Write a three-level call chain where the deepest level parses a number. Make the middle level catch the NumberFormatException and rethrow an IllegalStateException naming the row and column, with the original as the cause. Print e.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