Java · Exceptions

Java Exception Handling — Checked vs Unchecked, try/catch & Best Practices

5 min read Updated 2026-06-18

Practice Exception Handling interview questions

Java exception handling

Exceptions are how Java signals that something went wrong, separating error-handling code from the main logic. Handle them well and your programs fail safely and diagnosably; handle them poorly — swallowing errors, catching too broadly, leaking resources — and you get silent corruption that's miserable to debug. This guide covers the hierarchy, checked vs unchecked, the mechanics, and the practices that matter.

The exception hierarchy

Everything throwable descends from Throwable, which splits into:

  • Error — serious, usually unrecoverable JVM problems (OutOfMemoryError, StackOverflowError). Don't catch these.
  • Exception — application-level conditions you may handle.
    • RuntimeException and subclasses are unchecked.
    • All other Exception subclasses are checked.
Throwable
├── Error                 (unchecked — don't catch)
└── Exception
    ├── RuntimeException   (unchecked)
    └── IOException, SQLException...  (checked)

So "checked vs unchecked" is simply about whether the class sits under RuntimeException/Error.

Checked vs unchecked

  • Checked exceptions are verified by the compiler: a method must either catch them or declare throws. They represent recoverable, expected conditions (IOException, SQLException).
  • Unchecked exceptions aren't compiler-enforced — usually programming bugs (NullPointerException, IllegalArgumentException, ArrayIndexOutOfBoundsException).
void read() throws IOException {     // checked -> must declare or catch
  Files.readString(Path.of("x"));
}

The guideline: checked for conditions a caller can reasonably recover from, unchecked for bugs and contract violations. Modern frameworks (Spring) lean heavily toward unchecked to avoid throws clutter.

try, catch, finally

try wraps risky code, catch handles specific types, and finally runs always — even after a return — for cleanup. Order catch blocks specific-before-general, or the compiler complains.

try {
  read();
} catch (FileNotFoundException e) { // specific first
  recoverMissing();
} catch (IOException e) {            // broader after
  log(e);
} finally {
  cleanup();                         // always runs
}

finally runs in virtually all cases — only System.exit(), a JVM crash, or an infinite loop skip it. Never put a return/throw in finally: it overrides the try's result and silently swallows pending exceptions — a notorious bug.

try-with-resources

For anything that needs closing (streams, connections, locks), declare it in a try-with-resources; it auto-closes in reverse order when the block exits, normally or via exception.

try (var br = Files.newBufferedReader(path);
     var conn = dataSource.getConnection()) {
  return br.readLine();
} // br and conn closed automatically

The resource must implement AutoCloseable. This replaces error-prone manual finally cleanup and correctly handles suppressed exceptions: if both the body and close() throw, the body's exception is primary and the close exception is attached via getSuppressed() — nothing is lost. Multi-catch (catch (IOException | SQLException e)) collapses identical handlers.

throw vs throws, and custom exceptions

throw is a statement that raises an exception now; throws is a method clause declaring which checked exceptions may propagate. Create custom exceptions by extending Exception (checked) or RuntimeException (unchecked), passing the message and cause to super.

public class InsufficientFundsException extends RuntimeException {
  public InsufficientFundsException(int shortfall) {
    super("Short by " + shortfall + " cents");
  }
}

Decide checked vs unchecked by whether the caller can recover. Custom exceptions let you carry domain data and let callers catch precisely.

Exception chaining and translation

When you catch a low-level exception and rethrow a higher-level one, preserve the original as the cause so the full diagnostic trail survives. Exception translation converts low-level exceptions into ones appropriate to the current layer (a service shouldn't leak SQLException).

try {
  jdbc.query();
} catch (SQLException e) {
  throw new DataAccessException("load failed", e); // e becomes the cause
}

The stack trace then shows Caused by:. This is exactly what Spring's DataAccessException hierarchy does.

NullPointerException and Optional

An NPE happens when you dereference null — call a method, access a field, index an array, or unbox a null wrapper. Java 14+ gives helpful messages naming the exact null expression. Prevent NPEs with Objects.requireNonNull, constants-on-the-left ("x".equals(s)), and Optional for maybe-absent return values:

Optional<User> user = repo.findById(id);
String name = user.map(User::name).orElse("unknown");

Use Optional as a return type — not for fields, parameters, or collections (return an empty collection instead).

Best practices

  • Catch specific exceptions, not bare Exception/Throwable (which hides bugs and catches Errors you can't handle).
  • Never swallow — an empty catch hides failures and loses diagnostics. Handle, log, or rethrow.
  • Throw early, catch late — validate inputs at the boundary; handle where you have context to recover.
  • Don't use exceptions for control flow — building the stack trace is expensive.
  • Preserve the cause when wrapping, and log once at the boundary, not at every layer.
// swallowed — silent failure
try { risky(); } catch (Exception e) {}
// at minimum, log with the stack trace
try { risky(); } catch (Exception e) { log.error("risky failed", e); throw e; }

Recap

Java exceptions descend from Throwable (Error vs Exception, the latter split into checked and unchecked). Use try/catch/finally with specific-first ordering, try-with-resources for automatic cleanup and suppressed-exception handling, and chaining/translation to preserve causes across layers. Reserve checked exceptions for recoverable conditions, lean on Optional to dodge NPEs, and follow the practices — catch narrowly, never swallow, throw early/catch late, log once. Done right, exception handling makes failures safe and debuggable instead of mysterious.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.