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.
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.
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.
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
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.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.