Course outline · 0% complete

0/29 lessons0%

Course overview →

Exceptions

lesson 8-2 · ~6 min · 23/29

When things go wrong

Real programs run on hostile input: files that are missing, network calls that time out, users who type abc where a number belongs. A server that dies on the first bad request is useless, so every serious codebase has a strategy for failure, and exceptions are Java's.

An exception is an object thrown when an operation cannot proceed, such as dividing by zero, indexing past an array's end, or parsing "abc" as a number. Left uncaught it kills the program with a stack trace.

To handle one, use try and catch, which are Java's try and except:

try {
  int n = Integer.parseInt(input);
  System.out.println(n * 2);
} catch (NumberFormatException e) {
  System.out.println("not a number: " + e.getMessage());
} finally {
  System.out.println("runs either way");
}

The catch names the exception type it handles, and e.getMessage() carries the details. finally runs whether or not anything was thrown, which is where cleanup such as closing a file belongs.

Multiple catch blocks can be stacked, most specific first, because the first matching one wins and a broad catch (Exception e) placed early would swallow everything after it.

Surviving a bad index

An out-of-bounds read, a catch that absorbs it, and a program that keeps going.

public class Main {
  public static void main(String[] args) {
    int[] data = new int[3];
    try {
      System.out.println(data[5]);
    } catch (ArrayIndexOutOfBoundsException e) {
      System.out.println("caught: " + e.getMessage());
    } finally {
      System.out.println("cleanup runs");
    }
    System.out.println("program continues");
  }
}

Output

caught: Index 5 out of bounds for length 3
cleanup runs
program continues

The println inside the try never printed anything, because the exception was thrown while evaluating data[5], before the call happened. Everything after the throw inside a try block is skipped.

The message is written by the library and names both the bad index and the real length, which is often enough to fix the bug without a debugger. The last line proves the program survived, which is the difference between a handled failure and a crash.

Throwing, and the two families of exception

Your own code throws with throw:

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

Java splits exceptions into two families:

FamilyExamplesCompiler requirement
unchecked, the RuntimeException subclassesIllegalArgumentException, NullPointerExceptionnone, catching is optional
checkedIOException from file readingevery caller must catch it or declare throws

Unchecked exceptions signal programming bugs, and the fix is usually to correct the code rather than to catch anything. Checked exceptions signal expected outside failures, and the compiler forces every caller to either catch the exception or declare throws IOException on its own signature and pass the problem up.

Python has no checked exceptions at all, so this enforcement will be new. The reasoning is that a missing file is not a bug in your code, it is a fact of the outside world, and the compiler refuses to let any caller simply forget that it can happen.

Reading a stack trace

When an exception goes uncaught, Java prints a stack trace: the exception's type and message, then the chain of method calls that was in progress, innermost first.

Learning to read one is the highest-value debugging skill in Java, because it names the exact line that failed:

Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.divide(Main.java:7)
    at Main.main(Main.java:3)

Read it top down for three facts. What went wrong is ArithmeticException: / by zero, where is line 7 of Main.java inside divide, and who called it is line 3 in main.

The first line mentioning your own file is almost always where to look. Lines from java.util and friends are the library doing its job of refusing bad input, so the bug is in the code that handed it that input.

A trace that runs to fifty lines is normal in a framework, and most of those lines are the framework calling itself. Scanning for your own package name turns the wall of text into one useful line.

call stack main, line 3 divide, line 7 / by zero unwinds printed trace ArithmeticException at Main.divide(Main.java:7) at Main.main(Main.java:3) a catch anywhere on the way up stops the unwinding and the program continues from there
An uncaught exception unwinds the call stack frame by frame, and the printed trace lists those frames innermost first.

A checked exception with nowhere to go

A method that calls a file-reading API throwing the checked IOException, with no try/catch and no throws clause, does not compile.

Checked exceptions are part of a method's contract. The compiler requires every caller either to handle the exception or to pass it up explicitly:

static String read(String path) throws IOException {  // pass it up
  return Files.readString(Path.of(path));
}

Declaring throws IOException moves the obligation to whoever calls read, and eventually some layer has to decide what a missing file means for the user.

Unchecked exceptions in the RuntimeException family carry no such requirement, which is why null dereferences and bad array indexes compile silently. The compiler cannot predict them, so it does not ask.

Throwing and catching your own exception

A guarded divide, called once successfully and once inside a try.

public class Main {
  static int divide(int a, int b) {
    if (b == 0) {
      throw new IllegalArgumentException("cannot divide by zero");
    }
    return a / b;
  }

  public static void main(String[] args) {
    System.out.println(divide(10, 2));
    try {
      System.out.println(divide(10, 0));
    } catch (IllegalArgumentException e) {
      System.out.println("error: " + e.getMessage());
    }
  }
}

Output

5
error: cannot divide by zero

The first call prints 5, which is integer division from lesson 2-3 rather than 5.0. The second never returns a value at all, since throw abandons the method immediately.

The message passed to the constructor is what e.getMessage() later returns, so writing a specific one is worth the seconds it costs. IllegalArgumentException is unchecked, which is why divide needs no throws clause even though it clearly can fail.