Layers of Logic

12.1

Exceptions, Errors, and the Stack Trace

A method that fails has to tell the one that called it. Returning a value cannot do that job, so Java unwinds the call stack instead, and the wall of text it prints is a map of exactly how it got there.

Core20 min read4 exercises
01

Previously on

Section 5.1 showed the call-stack model: main calls a, a calls b, and each invocation has a frame that completes when its method returns.

That is the machinery this whole phase runs on. An exception does not return. It unwinds, and the trace it prints is that stack, written down.

02

The problem

Write a method that divides two numbers.

int divide(int a, int b) {
    return a / b;
}

Call it with b set to zero and the program stops. Before that, it prints something like this:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Calc.divide(Calc.java:4)
	at Calc.compute(Calc.java:9)
	at Calc.main(Calc.java:14)

The question is what divide should have done instead.

Try returning a special value. Return -1 when the division fails:

int divide(int a, int b) {
    if (b == 0) return -1;
    return a / b;
}

Now divide(-5, 5) returns -1, and so does divide(5, 0). The caller cannot tell an answer from a failure, because -1 is a perfectly good answer to a division. Every int is. There is no spare value to steal.

Try returning null from a method that finds things:

Student findByRoll(int roll) { ... return null; }

You did this in Section 11.5 and saw the problem. The signature promises a Student, nothing makes the caller check, and the crash arrives later somewhere else.

Both attempts share one flaw. The answer and the complaint travel down the same channel, so they can be confused, and ignoring the complaint is easier than handling it.

Then there is the question of who should deal with it. divide knows something went wrong. It has no idea what to do about it. Whether a division by zero should be logged, retried, shown to a user or fatal depends entirely on why it was called, and divide cannot see that far.

The method that knows is somewhere further up the stack. It might be two calls up, or ten.

03

The idea

Java gives failures their own channel. A method that cannot continue throws, and normal execution stops there.

int divide(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("cannot divide by zero");
    }
    return a / b;
}

An exception is an object, made with new like any other. What makes it special is what happens next.

The stack unwinds. Java abandons divide and looks at whoever called it. If that method does not handle it, that one is abandoned too, and so on down the stack. It keeps going until something catches it or main runs out, and then the program stops and the trace is printed.

What happens when nothing catches it

  1. divide throwsA new exception object is created, holding a message and a snapshot of the current call stack.
  2. divide is abandonedIts frame is discarded. Nothing after the throw runs, and no value is returned.
  3. compute is askedDoes it have a catch for this? No, so it is abandoned too, and its frame goes.
  4. main is askedSame answer. Now there is nothing left below it.
  5. The JVM prints and stopsThe snapshot taken in step one is printed as the stack trace, and that thread ends.

Catching it happens where you know what to do:

try {
    int result = divide(10, 0);
    System.out.println(result);
} catch (ArithmeticException e) {
    System.out.println("cannot divide by zero, using 0 instead");
    result = 0;
}

The try block holds the code that might fail. The catch block runs only if it does, and only for the type it names. Notice that divide was not changed at all. Reporting and handling are now in different places, which was the point.

finally runs whatever happens:

Scanner in = null;
try {
    in = new Scanner(new File("data.txt"));
    return in.nextLine();
} catch (FileNotFoundException e) {
    return "no file";
} finally {
    if (in != null) in.close();      // runs on success, on failure, and on return
}

It runs after a normal finish, after a caught exception, and even after a return. That makes it the place for cleanup, and cleanup is why it exists.

Then Java 7 made most of that unnecessary:

try (Scanner in = new Scanner(new File("data.txt"))) {
    return in.nextLine();
} catch (FileNotFoundException e) {
    return "no file";
}

Anything declared in those brackets is closed for you, in every case. This is try-with-resources, it works on anything implementing AutoCloseable, and the streams from Section 8.1 all do.

Error and Exception are siblings, not parent and child.

Throwable                 the only thing throw and catch will accept
├── Error                 the machine is in trouble
│     OutOfMemoryError, StackOverflowError. Do not catch these.
└── Exception             your program is in trouble, and may recover

Error and Exception both extend Throwable, and neither extends the other. So catch (Exception e) cannot catch an OutOfMemoryError by accident, which is exactly what you want. A broad catch in your code should not be able to swallow the JVM telling you it has run out of memory.

04

Under the hood

Going deeper

A stack trace is the call stack, printed innermost first.

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Calc.divide(Calc.java:4)
	at Calc.compute(Calc.java:9)
	at Calc.main(Calc.java:14)

Read the top line for what, and go down to the first line mentioning your own code for where. In a real trace the first several frames are often inside the JDK, and the useful one is the first with your package name on it.

Then look further down for Caused by:. When an exception is wrapped, the original is printed underneath, and the real failure can be well below the line you read first. In a Spring or Hibernate trace, the last Caused by is usually the one you want.

The snapshot is taken when the object is made, not when it is thrown. That is why an exception created in one place and thrown in another shows the wrong line, and it is also why creating exceptions is not cheap: filling in the trace means walking the whole stack.

Now the thing that catches people. finally beats everything, including a return:

static int finallyWins() {
    try { throw new IllegalStateException("boom"); }
    finally { return 42; }
}

That method returns 42. The exception is gone. Not caught, not logged, not rethrown: discarded, because a return in finally replaces whatever the method was doing, and what it was doing was throwing.

The same applies to ordinary returns:

static int which() {
    try { return 1; }
    finally { return 2; }
}

Returns 2.

The return value is worked out before finally runs.

static String order() {
    StringBuilder sb = new StringBuilder();
    try {
        sb.append("try ");
        return sb.toString() + "|returned";
    } finally {
        sb.append("finally ");
    }
}

finally really does run, and the buffer really does end up holding try finally. The returned string is still try |returned, because sb.toString() was evaluated before finally got its turn. The value was already decided.

try-with-resources closes in reverse order:

try (Res a = new Res("A"); Res b = new Res("B")) { ... }
open A
open B
body
close B
close A

Last opened, first closed. It has to be that way: b may depend on a, so closing a first could leave b closing against something already gone.

And it keeps both exceptions. If the body fails and the close also fails, the body’s exception is the one thrown and the close failure is attached to it:

catch (Exception e) {
    e.getMessage();       // "body failed"
    e.getSuppressed();    // [close failed]
}

Old finally cleanup could not do this. A close() that threw inside finally replaced the original exception, and the real cause disappeared. Try-with-resources keeps both, and the second one is called suppressed.

05

What it costs

Exceptions are expensive to create, and the cost is the stack trace. Filling one in means walking every frame, so throwing in a loop is slow in a way that catching is not. Using exceptions for ordinary control flow, like ending a loop, is the classic way to make a program mysteriously slow.

They also break the shape of your code. A method with four things that can fail can leave through five different doors, and reading it means holding all of them in your head. Deeply nested try blocks are hard to follow for exactly this reason.

The most damaging habit is the empty catch:

try { risky(); } catch (Exception e) { }

That compiles, runs, and destroys the information. The failure happened, nobody was told, and the program carries on with whatever half finished state it was left in. When it eventually breaks, the trace points somewhere unrelated.

Catching too broadly does a quieter version of the same thing. catch (Exception e) around a big block catches the one you expected and also the NullPointerException from your own bug, and treats them the same way.

There is one more that only shows up later. An exception carries its stack trace, and a trace holds references to the objects in those frames. Keeping exceptions in a list, which people do when collecting errors, keeps all of that alive. Phase XIII explains why that matters.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. Why can a method not report a failure by returning a value?

    Show the answer

    Because every value it could return is one a caller might legitimately want.

    divide returning -1 to mean failure is broken the moment a real answer is -1. Returning 0 has the same problem. There is no int that means "this did not work", so the caller cannot tell the difference between an answer and a complaint.

    Even where a spare value exists, like null, nothing forces the caller to look. The signature says it returns a value, so ignoring the failure is the easiest thing to write.

    An exception is on a separate channel. It cannot be mistaken for an answer, and ignoring it is not something you can do by accident.

  2. Which end of a stack trace do you read first?

    Show the answer

    The top line for what went wrong, then straight to the first line mentioning your own code for where.

    The order is the call stack from Section 5.1, printed innermost first. Line one is the method that threw. The bottom is main. Everything in between is how you arrived.

    The first few frames are often inside the JDK, so the useful line is usually the first one with your package name on it. That is where your code called something that failed.

    Then look for Caused by: lower down. A wrapped exception puts the real cause there, and the original failure can be twenty lines below the one you read first.

  3. What separates an `Error` from an `Exception`, and why does `catch (Exception e)` not catch both?

    Show the answer

    An Exception is a problem in your program that your program might be able to do something about. A file is missing, a number will not parse, an index is out of range.

    An Error is a problem with the machine your program is running on. OutOfMemoryError, StackOverflowError. There is usually nothing sensible to do, because the thing you would do to recover also needs the resource that has run out.

    They are siblings. Both extend Throwable, and neither extends the other, so catch (Exception e) cannot catch an Error even by accident. That separation is deliberate: it means a broad catch in your code cannot quietly swallow the JVM telling you it is in trouble.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises90 pointsabout 95 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Read the Trace Backwards

Warm up·20 min·15 points

checkedex-12-1-a

Read the whole trace before touching anything. That is the habit this exercise is building.

The line that threw is not the line that is wrong. divide did exactly what it was asked. The mistake happened five frames away, in the method that chose the argument, and the trace shows you both if you read it as a path rather than an error message.

Do all three and compare. Different exception types, same shape of report, and the same reading order every time.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class ReadIt {
    static List<String> framesFrom(Runnable work)
    static String typeOf(Runnable work)
    static String messageOf(Runnable work)
    static List<String> divideByZeroFrames()
    static List<String> badIndexFrames()
    static List<String> nullFrames()
    static String methodThatThrew()
    static String methodWithTheMistake()
}

framesFrom runs the work, catches whatever comes out, and returns the method names from OUR class only, innermost first. Keep the level1 to level3 chain and divide and compute exactly as written.

What your program must do

  • Read the whole trace before changing anything
  • Say which line threw and which line is the mistake
  • Do the same for a bad index and a null reference
  • Say what order the frames are printed in
ReadIt.java
import java.util.*;

public class ReadIt {

    static int divide(int a, int b) { return a / b; }
    static int compute(int a) { return divide(100, a); }
    static void level3(int a) { compute(a); }
    static void level2(int a) { level3(a); }
    static void level1(int a) { level2(a); }

    // Run the work, catch whatever comes out, and return OUR method names,
    // innermost first.
    static List<String> framesFrom(Runnable work) { return List.of(); }  // TODO

    static String typeOf(Runnable work)    { return ""; }  // TODO
    static String messageOf(Runnable work) { return ""; }  // TODO

    static List<String> divideByZeroFrames() { return List.of(); }  // TODO
    static List<String> badIndexFrames()     { return List.of(); }  // TODO
    static List<String> nullFrames()         { return List.of(); }  // TODO

    // Which method threw, and which method actually made the mistake?
    static String methodThatThrew()      { return ""; }  // TODO
    static String methodWithTheMistake() { return ""; }  // TODO

    public static void main(String[] args) {
        // TODO: call level1(0) and read the WHOLE trace before doing anything
    }
}
Hint 1
t.getStackTrace() gives the frames as objects. Filter to your own class name, or the JUnit machinery fills the list.
Hint 2
Innermost first. The top is where it threw, the bottom is main, and the lines between are how it got there.
Hint 3almost the answer
divide threw and divide is correct. It was asked to divide by zero, five frames away, by the code that chose the argument.
What this is really testing

Whether a stack trace is a wall of text or a map. It is the call stack from Section 5.1 printed innermost first, and knowing that turns twenty lines into two useful ones.

B

The Return That Ate an Exception

Real work·25 min·25 points

checkedex-12-1-b

Predict all three, in writing, before you run anything.

The first one is the one that matters. A method that throws returns 42 instead, and there is no exception anywhere. Nothing was caught. Nothing was logged. The failure was deleted by a line that looks like tidy cleanup.

The third one is subtler and worth the time. finally ran, it changed the buffer, and the returned value did not change. Work out why before reading the hint, because it tells you exactly when the return value gets decided.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Swallow {
    static int one()
    static int two()
    static String three()
    static int oneFixed()
    static String oneThrows()
    static String oneFixedThrows()
    static String bufferAfterThree()
}

Keep one, two and three exactly as given, returns in finally and all. oneFixed does the same work with finally used for cleanup only, setting the shared cleanedUp flag. The two Throws methods report whether an exception escaped.

What your program must do

  • Predict all three return values before running
  • Say what happened to the exception in the first method
  • Explain the third result, where finally clearly ran and changed nothing
  • Rewrite the first so the exception survives and cleanup still happens
Swallow.java
public class Swallow {

    // Leave these three exactly as they are. PREDICT all three first.
    static int one() {
        try { throw new IllegalStateException("boom"); }
        finally { return 42; }
    }

    static int two() {
        try { return 1; }
        finally { return 2; }
    }

    static String three() {
        StringBuilder sb = new StringBuilder();
        try { sb.append("try "); return sb.toString() + "|end"; }
        finally { sb.append("finally "); }
    }

    static boolean cleanedUp = false;

    // The same as one(), with finally doing cleanup ONLY.
    static int oneFixed() { return 0; }  // TODO

    // Did an exception escape? "threw" or "no exception".
    static String oneThrows()      { return ""; }  // TODO
    static String oneFixedThrows() { return ""; }  // TODO

    // Did finally change the buffer? Return what the caller receives.
    static String bufferAfterThree() { return ""; }  // TODO

    public static void main(String[] args) {
        // TODO: predict all three, then run them
    }
}
Hint 1
A return in finally replaces whatever the method was doing. What the first one was doing was throwing, so the exception is discarded.
Hint 2
In the third, sb.toString() was evaluated before finally ran. The buffer really does end up holding both words, and the returned String was built from a copy taken earlier.
Hint 3almost the answer
The fix is to never return from finally. Put cleanup there and nothing else, or use try-with-resources, which does the cleanup with no finally block at all.
What this is really testing

Whether you know what finally can do to a method. A return inside it deletes exceptions silently, and nothing in the language warns you.

C

Close Them in the Right Order

Real work·25 min·25 points

checkedex-12-1-c

Write the finally version too. That is where the value is.

Try-with-resources looks like a shortcut until you write out what it replaced, and then it stops looking like syntax. A null check per resource, a nested try around each close, and careful thought about what happens if a close throws while an exception is already travelling.

The last part is the one nobody shows you. Two things fail at once and Java keeps both. Find the second one, and then think about what the finally version would have done with it.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Resources {
    static List<String> closeOrder()
    static List<String> closedAfterThrow()
    static String bodyExceptionMessage()
    static String suppressedMessage()
}

Res is a nested AutoCloseable that records open and close into a shared log, and can be built to throw on close. Each method clears the log at the right point and returns what happened. suppressedMessage returns "none" when nothing was suppressed.

What your program must do

  • Predict the order two resources are closed in
  • Throw inside the body and confirm both still close
  • Write the equivalent with finally and compare
  • Make both the body and a close throw, and find where the second one went
Resources.java
import java.util.*;

public class Resources {

    static final List<String> log = new ArrayList<>();

    static class Res implements AutoCloseable {
        final String name;
        final boolean failOnClose;
        Res(String name) { this(name, false); }
        Res(String name, boolean failOnClose) {
            this.name = name;
            this.failOnClose = failOnClose;
            // TODO: record "open " + name
        }
        @Override public void close() {
            // TODO: record "close " + name, then throw if failOnClose
        }
    }

    // Open two, do something, and report the order everything happened in.
    static List<String> closeOrder() { return List.of(); }  // TODO

    // Same, but the body throws. Are they still closed?
    static List<String> closedAfterThrow() { return List.of(); }  // TODO

    // Body throws AND close throws. Which one comes out, and where does the other go?
    static String bodyExceptionMessage() { return ""; }  // TODO
    static String suppressedMessage()    { return ""; }  // TODO

    public static void main(String[] args) {
        // TODO: predict the close order before running
        // TODO: write the same thing with finally instead, and count the lines
    }
}
Hint 1
Last opened, first closed. The second resource may depend on the first, so closing the first would leave the second with nothing under it.
Hint 2
The finally version needs a null check per resource and a nested try around each close, because a close that throws would otherwise replace the real exception.
Hint 3almost the answer
When both throw, the body's exception is the one you catch and the close failure is attached to it. e.getSuppressed() returns it, and old finally cleanup lost it completely.
What this is really testing

Whether try-with-resources looks like shorter syntax or like a different guarantee. It closes in reverse and it keeps both exceptions, and old finally cleanup could do neither.

D

Errors Are Not Yours

Hard·25 min·25 points

checkedex-12-1-d

Break it on purpose, and then try to catch it the obvious way.

catch (Exception e) does nothing here, and that is the design working. Error and Exception are siblings, so a broad catch in your code physically cannot swallow the JVM telling you it is in trouble. Confirm it with the type system rather than taking anyone’s word for it.

Then catch the Error itself, because you can, and think about what you would actually do in that block. Nearly everything you might try needs the resource that has run out.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class NotYours {
    static int forever(int depth)
    static int overflowDepth()
    static String caughtAsException()
    static String caughtAsThrowable()
    static boolean errorIsAnException()
    static boolean runtimeIsAnException()
    static boolean errorIsThrowable()
}

forever recurses and records how deep it got in a shared field. overflowDepth catches the StackOverflowError and returns the depth reached. caughtAsException has both a catch of Exception and a catch of StackOverflowError, and returns which one ran. The three hierarchy questions must be answered by asking the type system, not by returning a constant you looked up.

What your program must do

  • Cause a StackOverflowError on purpose and record the depth reached
  • Show that a catch of Exception does not catch it
  • Ask the type system about the hierarchy rather than trusting a diagram
  • Say why catching an Error is still a bad idea even though you can
NotYours.java
public class NotYours {

    static int deepestReached = 0;

    static int forever(int depth) { return 0; }  // TODO: recurse, recording the depth

    static int overflowDepth() { return 0; }  // TODO: catch the Error, return how deep it got

    // Put BOTH a catch of Exception and a catch of StackOverflowError around it.
    // Which one runs? Return "caught as Exception" or "only caught as Error".
    static String caughtAsException() { return ""; }  // TODO

    // And with a catch of Throwable instead.
    static String caughtAsThrowable() { return ""; }  // TODO

    // Ask the TYPE SYSTEM, do not return a constant you remembered.
    static boolean errorIsAnException()   { return true; }  // TODO
    static boolean runtimeIsAnException() { return false; } // TODO
    static boolean errorIsThrowable()     { return false; } // TODO

    public static void main(String[] args) {
        // TODO: find how deep this machine goes, then try -Xss256k and compare
    }
}
Hint 1
Exception.class.isAssignableFrom(Error.class) answers it directly. They are siblings under Throwable, so neither catch can reach the other.
Hint 2
The depth is a fact about your stack size, not about Java. It changes between machines and with the -Xss flag, which tells you what the limit is made of.
Hint 3almost the answer
You can catch a StackOverflowError and it is still a bad idea. Recovering usually means calling something, and calling something needs stack, which is the resource that has run out.
What this is really testing

Whether the split between Error and Exception feels arbitrary. Once you have caused a StackOverflowError on purpose, the reason you cannot usefully catch it is obvious.

08

After the credits

There is a split in the hierarchy this section did not open.

Files.readString(path);          // will not compile without a try or a throws
Integer.parseInt("abc");         // compiles fine, throws at run time

Both can fail. One of them the compiler forces you to deal with, and the other it says nothing about. The difference is checked and unchecked, and it is the most argued about decision in Java’s design.

Section 12.2 takes the hierarchy apart, explains throw against throws, and shows how to write an exception of your own that carries what the caller actually needs. It also makes the case against checked exceptions, which every language designed after Java has quietly agreed with.

Threads you opened in this section