Exceptions

Java · Practical · 14 min

Robust programs must handle failure deliberately, and Java's checked exceptions force you to think about it.

When something goes wrong, Java code throws an exception object, which unwinds the call stack until a matching catch handles it. If nothing does, the thread dies with a stack trace — read it from the top: the first line says what happened, the first frame in your code says where.

The hierarchy matters:

  • Exception subclasses (except RuntimeException) are checked: the compiler forces callers to catch them or declare throws. Examples: IOException, SQLException — failures outside your control.
  • RuntimeException subclasses are unchecked: programming errors or invalid input, such as NullPointerException, IllegalArgumentException, IllegalStateException.
  • Error (e.g. OutOfMemoryError) signals JVM-level problems you normally don't catch.

Catch the most specific type you can actually handle; combine types with a multi-catch catch (IOException | ParseException e). Never swallow an exception with an empty catch block — at minimum log it.

When you translate a low-level exception into your own, pass the original as the cause (throw new ConfigException("bad config", e)) so the stack trace keeps the root cause.

finally runs whether or not an exception occurred. For resources such as files, sockets and database connections, prefer try-with-resources, which closes anything AutoCloseable automatically, in reverse order, even on failure:

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}

Use exceptions for exceptional situations, not ordinary control flow; for "not found" results, an Optional return is often clearer.

Example

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;

public class Errors {
    static class ConfigException extends RuntimeException {
        ConfigException(String msg, Throwable cause) { super(msg, cause); }
    }
    static int readPort(String text) {
        try (var reader = new BufferedReader(new StringReader(text))) {
            return Integer.parseInt(reader.readLine().strip());
        } catch (IOException | NumberFormatException e) {
            throw new ConfigException("invalid port: " + text, e); // keep the cause
        }
    }

    public static void main(String[] args) {
        System.out.println(readPort("8080"));          // 8080
        try {
            readPort("eighty");
        } catch (ConfigException e) {   // invalid port: eighty <- NumberFormatException
            System.out.println(e.getMessage() + " <- " + e.getCause().getClass().getSimpleName());
        }
    }
}

Remember

  • Checked exceptions must be caught or declared; unchecked ones signal bugs or bad input
  • Catch specific types, never swallow silently, and chain the cause when wrapping
  • try-with-resources closes AutoCloseable resources automatically

Watch out for

  • catch (Exception e) {} — hides real failures and makes debugging miserable.
  • Wrapping an exception without passing the cause, losing the original stack trace.

Check yourself

  1. Which exception is checked?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/tutorial/essential/exceptions/index.html