Java Exceptions Explained: NPE, ClassCast, and the Ones After Them
Java 14+ NPEs name the exact null expression. For everything else: the exception type tells you the family, the first frame in your code tells you the site.
NullPointerException (NPE)
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because "name" is null
at com.app.User.display(User.java:23)Modern Java tells you everything: which method was called and which variable was null. With older JVMs, you get only the line number.
The causes, in observed frequency: (1) a method returned null (map.get with a missing key, repository lookups, JSON fields absent); (2) a field never initialized on some code path; (3) autoboxing — Integer null unboxed into int (int x = map.get("missing"); throws with a confusing trace). Fixes per cause: use Optional or an explicit null check at the source; initialize fields in the constructor; use map.getOrDefault(...) or check containsKey before unboxing. And note: Objects.requireNonNull(arg, "arg") at the top of a public method turns a vague NPE deep in the call stack into a precise one at the boundary — a habit worth adopting.
ClassCastException
Object o = "hello";
Integer i = (Integer) o; // ClassCastException: class String cannot be cast to IntegerWith generics, this usually leaks through raw types or unchecked conversions:
List list = new ArrayList(); // raw type — no checking
list.add("string");
Integer i = (Integer) list.get(0); // ClassCastException at runtimeFix: eliminate raw types (List<String>), and where external data forces a cast, check first: if (o instanceof Integer i) { use(i); } — pattern matching (Java 16+) makes the check-and-cast a single safe idiom.
ConcurrentModificationException
for (String s : list) {
if (s.startsWith("x")) list.remove(s); // ConcurrentModificationException
}Why: you modified the collection while iterating it — a fail-fast guard, not an actual threading problem (though real concurrency causes it too).
Fixes, best to worst: (1) list.removeIf(s -> s.startsWith("x")); — the intended API; (2) iterate over a copy when removal logic is complex: new ArrayList<>(list); (3) use an explicit Iterator and call it.remove(); (4) never list.remove() inside a for-each — it is always a bug. For genuinely concurrent code, CopyOnWriteArrayList or ConcurrentHashMap change the semantics deliberately.
IllegalArgumentException / IllegalStateException
The good exceptions: they mean a caller (IAE) or a lifecycle (ISE) contract was violated, and they usually come with a message that names the rule. The fix is upstream: validate before calling, or check state before transitioning (if (!started) throw new IllegalStateException("start() first")). When writing libraries, prefer these over NPE for invalid inputs — they carry intent.
ArrayIndexOutOfBounds / StringIndexOutOfBounds
Off-by-one at index access, or an empty collection assumed non-empty. Fixes: loop with < arr.length, use the enhanced for, guard with if (!list.isEmpty()), and replace manual index math with subList/streams where it expresses the intent. Boundary tests (empty, one element) catch these before production does.
NumberFormatException
Integer.parseInt("12.5"); // NumberFormatException: For input string: "12.5"Fix: validate/trim input first, use Double.parseDouble when decimals are legitimate, and wrap user-supplied conversions in try/catch that reports WHICH value failed — NumberFormatException should carry the offending string (and it does, in the message).
Checked vs Unchecked: The Decision Everyone Gets Wrong
| Checked (extends Exception) | Unchecked (extends RuntimeException) | |
|---|---|---|
| Examples | IOException, SQLException | NPE, IAE, ISE |
| Compiler forces handling | Yes | No |
| Use for | Recoverable, caller-actionable conditions | Programming errors, contract violations |
| The mistake | Making callers handle a bug (wrap in RuntimeException) | Catching broad RuntimeException as flow control |
The practical rule: if the caller can do something meaningful (retry, fallback, report), checked. If the only correct response is "fix the code", unchecked. And never write catch (Exception e) {} to make the compiler stop complaining — that converts a loud design problem into a silent one.
Reading a Java Stack Trace Fast
Read the top two frames in your packages, skip the 15 lines of framework reflection, and note the "Caused by:" chain — the FIRST cause is the root. When the trace is full of InvocationTargetException or proxy classes (Spring, reflection), the real exception is in the Caused by section, not the top.