intermediate20 min

Exceptions: Checked, Unchecked, and Handling Them Well

Java's two families of exceptions, why the compiler forces you to acknowledge one but not the other, and how to fail in a way that actually helps whoever hits the error.

What you'll learn

  • Explain the difference between checked and unchecked exceptions and why the distinction exists
  • Write a try/catch/finally block that handles failure correctly, without swallowing information
  • Design a custom exception type for a specific failure case

Prerequisites

Explanation

Java splits exceptions into two families with genuinely different rules. Checked exceptions (subclasses of Exception but not RuntimeException — e.g. IOException) must be either caught or declared with throws in a method's signature; the compiler enforces this, and code that ignores a checked exception simply does not compile. They represent conditions a well-written caller is expected to anticipate and recover from — a file that might not exist, a network call that might fail — situations that are a normal, foreseeable part of the operation, not a bug. Unchecked exceptions (RuntimeException and its subclasses — NullPointerException, IllegalArgumentException, IndexOutOfBoundsException) need no throws declaration and no forced catch; they typically represent programming errors — a bug — that no amount of catching fixes, only prevents from being visible.

A try/catch/finally block runs finally always — whether the try block succeeds, throws, or even returns early — which makes it the correct place for cleanup that must happen no matter what (though try-with-resources, covered later in this course, is the more modern, safer tool for that specific job). Catching an exception and doing nothing with it (catch (Exception e) {}) — sometimes called "swallowing" the exception — is one of the most damaging habits in Java code: it makes a real failure invisible, so the program silently continues in a broken state instead of failing where the problem is easy to diagnose. At minimum, a caught exception you can't fully recover from should be logged with enough context to debug it, or re-thrown (possibly wrapped in a more meaningful exception type) — never discarded.

Writing a custom exception (class InvalidEnrollmentException extends RuntimeException { ... }) is the right move once "throw a generic IllegalArgumentException with a message" stops being specific enough for callers to meaningfully react to different failure causes differently — a custom type lets a catch (InvalidEnrollmentException e) block target exactly that failure, distinct from any other IllegalArgumentException a totally unrelated part of the code might throw. A custom exception's constructor should always pass its message (and, when wrapping another exception, that cause) up to the superclass constructor, so the full context is preserved in the exception chain.

Example

Checked-vs-unchecked modeled as two error classes in JS, with the finally-always-runs guarantee shown explicitly.

class RecoverableError extends Error {}  // models a checked exception -- expected to be handled
class ProgrammerError extends Error {}   // models an unchecked exception -- a bug, not expected input

function loadFile(path) {
  if (path === "missing.txt") throw new RecoverableError("file not found: " + path);
  return "file contents";
}

function process(path) {
  try {
    return loadFile(path);
  } catch (e) {
    if (e instanceof RecoverableError) {
      console.log("Recovered: using default content instead.");
      return "default content";
    }
    throw e; // an unexpected error type -- re-throw, never swallow silently
  } finally {
    console.log("cleanup always runs, success or failure");
  }
}

console.log(process("missing.txt")); // logs cleanup, then "default content"

Try it yourself

Change the catch block to do nothing (an empty catch {}) and observe how the failure becomes invisible.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write safeDivide(a, b) modeling a Java method that throws a custom-style error for division by zero (throw new Error('cannot divide by zero')) and otherwise returns a / b. Then write divideAll(pairs) that attempts safeDivide on each [a,b] pair, collecting successful results and SKIPPING (not crashing on) failures, returning only the successful results array.

Checks: safeDivide computes correctly for valid input · safeDivide throws for division by zero · divideAll collects only the successful results, skipping failures

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Model a custom exception hierarchy: class InvalidEnrollmentError extends Error with a constructor(reason) setting this.name = 'InvalidEnrollmentError' and calling super(reason). Write validateEnrollment(learnerId, courseId) throwing InvalidEnrollmentError with a specific reason if either is falsy, otherwise returning true.

Checks: valid enrollment data passes validation · missing learnerId throws the custom error type · missing courseId throws with a specific, correct message

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • `catch (Exception e) {}` with an empty body -- this hides real failures instead of handling them, and is almost always a bug in its own right, not a fix.
  • Catching Exception broadly when a specific exception type is what's actually expected -- this can silently swallow unrelated bugs that happen to also be exceptions, not just the anticipated failure case.
  • Using exceptions for ordinary control flow (e.g. throwing to signal 'not found' in a hot loop) -- exceptions carry real performance cost from capturing a stack trace, and a simple return value (like Optional or a null check) is usually the better tool for an expected, common outcome.

Knowledge check

Knowledge check

1. Why must a checked exception be caught or declared with throws, while an unchecked exception does not?
2. What is guaranteed to run in `try { ... } catch (Exception e) { ... } finally { cleanup(); }`, no matter what happens in try or catch?
3. What's wrong with `catch (Exception e) { /* nothing */ }`?

Takeaway

Checked exceptions are the compiler forcing you to acknowledge foreseeable failure; unchecked exceptions are almost always bugs, not conditions to routinely catch. Never catch and discard an exception silently — log it, handle it meaningfully, or let it propagate.

Summary

Checked exceptions must be caught or declared; unchecked exceptions need neither. finally always runs. Custom exception types let callers react specifically to a named failure instead of a generic one. Swallowing exceptions silently is one of the most damaging habits in error handling.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.