Skip to content
Java advanced 4 min read

Exception Handling

Exceptions let a program signal and recover from abnormal conditions without polluting normal control flow with error codes. Java’s exception model is explicit and type-safe: methods declare what can go wrong, and the compiler enforces handling for an important category of failures. Used well, exceptions make failure paths as clear as success paths.

The Exception Hierarchy

Everything throwable descends from Throwable, which splits into Error (JVM-level problems you should not catch) and Exception (recoverable conditions). Exception further splits into checked exceptions and RuntimeException (unchecked).

            Throwable
           /         \
        Error       Exception
      (unchecked)   /        \
                 checked   RuntimeException
                            (unchecked)
   e.g. OutOfMemoryError    e.g. IOException   e.g. NullPointerException
        StackOverflowError       SQLException       IllegalArgumentException
CategoryCompiler enforces?ExamplesWhen to use
CheckedYes — must catch or declareIOException, SQLExceptionRecoverable, external failures
Unchecked (RuntimeException)NoNullPointerException, IllegalArgumentExceptionProgramming errors / bugs
ErrorNo (don’t catch)OutOfMemoryErrorFatal JVM conditions

Note: Checked exceptions force the caller to acknowledge failure. Unchecked exceptions usually indicate a bug that should be fixed, not handled.

try / catch / finally

public int parse(String s) {
    try {
        return Integer.parseInt(s);
    } catch (NumberFormatException e) {
        System.out.println("Not a number: " + s);
        return -1;
    } finally {
        System.out.println("cleanup runs no matter what");
    }
}

The finally block executes whether or not an exception is thrown — ideal for releasing resources. It even runs when the try block returns.

Warning: Never return from a finally block. It silently discards any exception or return value from the try block, hiding failures.

try-with-resources

For anything implementing AutoCloseable, try-with-resources closes it automatically, in reverse order of declaration, even on exception. This replaces verbose finally cleanup.

import java.io.*;

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    return reader.readLine();
} catch (IOException e) {
    System.out.println("read failed: " + e.getMessage());
    return null;
}
// reader.close() is called automatically

This also handles suppressed exceptions: if both the body and close() throw, the body’s exception propagates and the close exception is attached via getSuppressed().

Multi-catch

When several exception types share handling logic, combine them with | instead of duplicating blocks.

try {
    riskyOperation();
} catch (IOException | SQLException e) {
    log.error("operation failed", e);
    throw new ServiceException("could not complete", e);
}

The combined exception variable is implicitly final; you cannot reassign e.

Custom Exceptions

Define your own exceptions to convey domain meaning. Extend RuntimeException for unchecked, Exception for checked. Always preserve the cause.

public class InsufficientFundsException extends RuntimeException {
    private final double shortfall;

    public InsufficientFundsException(double shortfall, Throwable cause) {
        super("Short by $" + shortfall, cause);
        this.shortfall = shortfall;
    }

    public double getShortfall() {
        return shortfall;
    }
}

throw vs throws

These look similar but do opposite jobs.

  • throw is a statement that raises an exception instance now.
  • throws is a clause in a method signature declaring which checked exceptions may escape.
public void withdraw(double amount) throws AccountException { // declares
    if (amount <= 0) {
        throw new IllegalArgumentException("amount must be positive"); // raises
    }
}

Best Practices

  • Never swallow exceptions with an empty catch {} — at minimum log them with full context.
  • Catch the most specific exception type; avoid catching bare Exception or Throwable.
  • Always preserve the original cause when wrapping (new XException(msg, cause)).
  • Use try-with-resources for every AutoCloseable.
  • Throw unchecked exceptions for programming errors; reserve checked exceptions for recoverable conditions.
  • Don’t use exceptions for normal control flow — they are expensive and obscure intent.
  • Fail fast: validate arguments early and throw IllegalArgumentException/NullPointerException.

Interview Questions

Q: What is the difference between checked and unchecked exceptions? Checked exceptions (subclasses of Exception but not RuntimeException) must be caught or declared with throws; the compiler enforces this. Unchecked exceptions (RuntimeException and Error) need no declaration and usually signal bugs or fatal conditions.

Q: What happens if both the try block and finally throw an exception? The exception from finally replaces the one from try, which is lost. This is why you should avoid throwing or returning from finally. Try-with-resources avoids the problem by suppressing the close exception rather than discarding the original.

Q: Why prefer try-with-resources over a finally block? It is less error-prone: resources are closed automatically in reverse order, even on exception, and the original exception is preserved while the close exception is attached as suppressed. Manual finally cleanup is verbose and easy to get wrong.

Q: When should you create a custom exception? When existing types don’t convey the domain failure clearly or you need to attach extra data (like a shortfall amount). Custom exceptions improve readability and let callers handle specific conditions distinctly.

Last updated June 1, 2026
Was this helpful?