JAVA / EXCEPTIONS
try, catch and catching the right type
Wrap only the statement that can fail and choose a catch type narrow enough to handle it without swallowing unrelated bugs.
What you will learn
- Read a catch clause as an instanceof test, not as a generic error hook
- Choose the narrowest exception type your handler can genuinely recover from
- Predict which exceptions a supertype clause will also swallow
- Keep the try block around only the statements that can actually fail
Understanding try, catch and catching the right type
A try block marks a region the runtime watches, and a catch clause is not a general "if something goes wrong" hook but a type test. When a statement throws, execution stops at that statement and the runtime asks one question about the enclosing try: is the thrown object an instance of the type named in the catch parameter? If yes, the catch body runs and the exception is finished; if no, the exception ignores that clause entirely and keeps travelling up the call stack, out of the method.
Because control never returns to the point of failure, every statement after the throwing one inside the try is skipped, including assignments that later code was counting on. That is the practical reason to keep the try small and the catch type specific: a clause named NumberFormatException tells you exactly one thing went wrong, that the text was not a number, and you can substitute a default with confidence. A clause named Exception tells you nothing about what failed, so any recovery you write is a guess applied to an unknown situation.
Exception types form a tree under Throwable, and catching a type catches every descendant of it. NumberFormatException extends IllegalArgumentException, which extends RuntimeException, which extends Exception, so each step up that chain quietly widens what your handler claims to fix until it is also claiming to fix NullPointerException and ArrayIndexOutOfBoundsException from your own bugs. Widening costs precision too: the compiler treats the catch parameter as the type you declared, so you only get the members of that broad type even though a far more specific object is in the variable.
public class RightType {
static int percentOf(String raw, int total) {
try {
int share = Integer.parseInt(raw);
return 100 * share / total;
} catch (NumberFormatException e) {
System.out.println("caught NumberFormatException: " + e.getMessage());
return 0;
}
}
public static void main(String[] args) {
System.out.println(percentOf("25", 50));
System.out.println(percentOf("twenty", 50));
try {
System.out.println(percentOf("25", 0));
} catch (ArithmeticException e) {
System.out.println("escaped percentOf: " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}A catch clause is a type filter, so the class you name decides both which failures you handle and which keep unwinding the stack.
Worked examples
Catching a supertype
Shows that a catch clause matches any subclass of the type it names, while the thrown object keeps its real type.
public class CatchBySupertype {
public static void main(String[] args) {
try {
Integer.parseInt("12ab");
} catch (IllegalArgumentException e) {
System.out.println("clause matched: IllegalArgumentException");
System.out.println("object thrown: " + e.getClass().getName());
System.out.println("instanceof NumberFormatException: " + (e instanceof NumberFormatException));
}
}
}Example explained
Line 1Integer.parseInt throws NumberFormatException, and since that class extends IllegalArgumentException the clause's type test succeeds.
Line 2e.getClass().getName() proves the catch type did not change the object; a NumberFormatException is still what is sitting in e.
Line 3The compiler, however, only sees e as an IllegalArgumentException, so reaching subclass-specific API would need instanceof plus a cast.
Line 4The cost of widening is that any other IllegalArgumentException raised in that try lands in this same branch and gets the same treatment.
A broad catch misdiagnosing a bug
Shows how catch (Exception e) turns a missing null check into a false report about bad input.
public class TooBroad {
static int trimmedLength(String s) {
try {
return s.trim().length();
} catch (Exception e) {
System.out.println("reported as bad input, actually " + e.getClass().getSimpleName());
return 0;
}
}
public static void main(String[] args) {
System.out.println(trimmedLength(" hi "));
System.out.println(trimmedLength(null));
}
}Example explained
Line 1The first call trims to "hi" and returns 2; nothing in that path can throw, so the try adds no value for valid input.
Line 2The second call reaches s.trim() with s null, which throws NullPointerException, a defect in the caller rather than a data problem.
Line 3catch (Exception e) matches it because NullPointerException is a descendant of Exception, so the bug is relabelled as bad input.
Line 4Returning 0 makes the failure indistinguishable from an empty string, so the caller keeps computing on a value that was never measured.
Important notes
Catching a checked type the block cannot throw is rejected at compile time with "exception java.io.IOException is never thrown in body of corresponding try statement", but Exception and unchecked types are always accepted, which is exactly why over-broad clauses slip through unnoticed.
Do not widen the parameter to Throwable or Error: OutOfMemoryError and StackOverflowError mean the JVM is already in trouble, and pretending to handle them at an arbitrary statement leaves the program in an unknown state.
Common mistakes
Reaching for catch (Exception e) by habit: a NullPointerException caused by a typo inside the try is reported as invalid input, the program continues on wrong data, and the real defect surfaces much later somewhere unrelated.
Leaving the catch body empty: execution resumes after the try as if the work had succeeded, and the next line reads a variable that still holds its pre-failure value.
Declaring the result variable inside the try and then using it in the catch or after the try: the try block is its own scope, so compilation stops with "cannot find symbol".
Try it yourself
Change, predict, then run
Write char secondChar(String s) that returns s.charAt(1) inside a try, and pick a catch type so that secondChar("a") returns '?' while secondChar(null) still throws out of the method. Then widen the parameter to Exception and observe how the null case is reported.
Open the Java workspaceCheck your understanding
A try block calls Integer.parseInt(raw) and then divides by a count that may be zero. Its only catch clause is catch (IllegalArgumentException e). The parse succeeds but the count is zero. What happens?
- The catch runs, because ArithmeticException and IllegalArgumentException are both unchecked
- The code does not compile, since the ArithmeticException is never caught
- The ArithmeticException does not match the clause, so it leaves the method and the catch body never runs
- The catch runs, and e.getClass() reports IllegalArgumentException
Show answer
Matching is a single instanceof test against the declared parameter type, and ArithmeticException extends RuntimeException directly, not IllegalArgumentException, so the clause is skipped and the exception continues up the stack. The first option is tempting because both classes are unchecked, but "unchecked" is a category, not a subtype relationship, and it plays no part in clause matching.