Common Java Exceptions
Common Java Exceptions
Most Java runtime failures cluster around a handful of exceptions. NullPointerException, ClassCastException, ArrayIndexOutOfBoundsException, ConcurrentModificationException — these appear in stack traces so frequently that recognising them on sight, knowing exactly what triggers each one, and knowing how to prevent them is a core production skill. This article covers every exception you are likely to encounter in a Java codebase and the pattern that fixes each one.
Overview of Common Exceptions
QUICK REFERENCE — MOST COMMON JAVA EXCEPTIONS:
UNCHECKED (RuntimeException subclasses):
NullPointerException — method called on null reference
ArrayIndexOutOfBoundsException — array access beyond valid index range
StringIndexOutOfBoundsException — String.charAt() beyond string length
ClassCastException — invalid cast between incompatible types
NumberFormatException — non-numeric string parsed as number
IllegalArgumentException — method called with invalid argument
IllegalStateException — object used in wrong lifecycle state
ArithmeticException — division by zero (integer arithmetic)
UnsupportedOperationException — mutation on read-only / fixed-size view
ConcurrentModificationException — collection structurally modified during iteration
StackOverflowError — infinite recursion exhausts call stack
(Error, not RuntimeException — different root)
CHECKED (Exception subclasses — must catch or declare):
IOException / FileNotFoundException — file or I/O operation fails
SQLException — database operation fails
ClassNotFoundException — Class.forName() cannot find class
InterruptedException — thread interrupted during wait/sleep
MODERN ADDITIONS:
NullPointerException (Java 14+) — helpful NPE messages naming the null field
When Each Exception Occurs
CAUSE SUMMARY:
NullPointerException → dereference a null reference
ArrayIndexOutOfBounds → index < 0 or index >= array.length
ClassCastException → cast to incompatible type (fails instanceof)
NumberFormatException → Integer.parseInt("abc") or similar
IllegalArgumentException → argument violates method's documented contract
IllegalStateException → call sequence out of order (not initialized yet)
ArithmeticException → integer division by 0 (not double — double returns Infinity)
UnsupportedOperationException → add/remove on List.of(), Arrays.asList(),
Collections.unmodifiableList() view
ConcurrentModificationException → for-each loop with list.remove() inside
StackOverflowError → recursive method with no reachable base case
IOException → file missing, stream closed, network down
ClassNotFoundException → Class.forName("com.example.Missing")
NullPointerException
NullPointerException (NPE) is the most common exception in Java codebases. It is thrown when code attempts to use a reference that holds null as if it pointed to an object — calling a method on it, accessing a field through it, or using it where a value is expected in autoboxing.
Java 14 introduced helpful NPE messages that name exactly which variable or expression was null, eliminating much of the guesswork in debugging.
1// File: NullPointerExceptionDemo.java
2
3import java.util.Map;
4import java.util.Optional;
5
6public class NullPointerExceptionDemo {
7
8 record UserProfile(String name, String email, Address address) {}
9 record Address(String city, String pinCode) {}
10
11 // WRONG — NPE if user is null, or user.address() is null, or user.address().city() is null
12 static String getCityUnsafe(UserProfile user) {
13 return user.address().city().toUpperCase(); // multiple NPE points
14 }
15
16 // CORRECT — null-check each level before dereferencing
17 static String getCitySafe(UserProfile user) {
18 if (user == null) return "unknown";
19 if (user.address() == null) return "no address";
20 if (user.address().city() == null) return "no city";
21 return user.address().city().toUpperCase();
22 }
23
24 // MODERN — Optional eliminates null checks for optional values
25 static Optional<String> getCityOptional(UserProfile user) {
26 return Optional.ofNullable(user)
27 .map(UserProfile::address)
28 .map(Address::city)
29 .map(String::toUpperCase);
30 }
31
32 // Autoboxing NPE — often missed by beginners
33 static int getScoreUnsafe(Map<String, Integer> scores, String userId) {
34 return scores.get(userId); // NPE if key is absent: unboxing null Integer to int
35 }
36
37 static int getScoreSafe(Map<String, Integer> scores, String userId) {
38 return scores.getOrDefault(userId, 0); // safe: returns 0 if absent
39 }
40
41 public static void main(String[] args) {
42
43 System.out.println("=== Null chain navigation ===");
44 UserProfile complete = new UserProfile("Priya", "priya@example.com",
45 new Address("Bengaluru", "560001"));
46 UserProfile noAddress = new UserProfile("Rohan", "rohan@example.com", null);
47
48 System.out.println(getCitySafe(complete));
49 System.out.println(getCitySafe(noAddress));
50 System.out.println(getCitySafe(null));
51
52 System.out.println();
53
54 System.out.println("=== Optional chaining ===");
55 System.out.println(getCityOptional(complete).orElse("unknown"));
56 System.out.println(getCityOptional(noAddress).orElse("unknown"));
57 System.out.println(getCityOptional(null).orElse("unknown"));
58
59 System.out.println();
60
61 System.out.println("=== Autoboxing NPE ===");
62 Map<String, Integer> scores = Map.of("user-001", 95, "user-002", 88);
63 System.out.println("user-001 score: " + getScoreSafe(scores, "user-001"));
64 System.out.println("user-003 score: " + getScoreSafe(scores, "user-003")); // absent key
65
66 try {
67 getScoreUnsafe(scores, "user-003"); // absent key → null → unboxing NPE
68 } catch (NullPointerException npe) {
69 System.out.println("NPE on unboxing absent map value: " + npe.getMessage());
70 }
71 }
72}Output:
=== Null chain navigation ===
BENGALURU
no address
unknown
=== Optional chaining ===
BENGALURU
unknown
unknown
=== Autoboxing NPE ===
user-001 score: 95
user-003 score: 0
NPE on unboxing absent map value: Cannot unbox null value
ArrayIndexOutOfBoundsException
Thrown when accessing an array using an index that is negative or greater than or equal to the array's length. The valid index range for an array of length n is always [0, n-1].
1// File: ArrayIndexDemo.java
2
3import java.util.Arrays;
4import java.util.List;
5
6public class ArrayIndexDemo {
7
8 // Most common cause: off-by-one in loops
9 static int sumUnsafe(int[] numbers) {
10 int total = 0;
11 for (int i = 0; i <= numbers.length; i++) { // WRONG: should be i < numbers.length
12 total += numbers[i]; // ArrayIndexOutOfBoundsException at i == numbers.length
13 }
14 return total;
15 }
16
17 static int sumSafe(int[] numbers) {
18 int total = 0;
19 for (int num : numbers) { // for-each eliminates index arithmetic entirely
20 total += num;
21 }
22 return total;
23 }
24
25 // Getting the last element — common off-by-one
26 static int lastElement(int[] numbers) {
27 if (numbers == null || numbers.length == 0) {
28 throw new IllegalArgumentException("Array must not be null or empty");
29 }
30 return numbers[numbers.length - 1]; // CORRECT: length-1, not length
31 }
32
33 public static void main(String[] args) {
34
35 int[] scores = {85, 92, 78, 95, 88};
36
37 System.out.println("=== Safe array operations ===");
38 System.out.println("Sum : " + sumSafe(scores));
39 System.out.println("Last : " + lastElement(scores));
40 System.out.println("Length : " + scores.length);
41 System.out.println("Index 0 : " + scores[0]);
42 System.out.println("Index 4 : " + scores[4]); // valid: length-1
43
44 System.out.println();
45
46 System.out.println("=== Boundary violation ===");
47 try {
48 int bad = scores[5]; // index 5, length is 5 — invalid
49 } catch (ArrayIndexOutOfBoundsException e) {
50 System.out.println("Caught: " + e.getMessage());
51 }
52 try {
53 int neg = scores[-1]; // negative index
54 } catch (ArrayIndexOutOfBoundsException e) {
55 System.out.println("Caught: " + e.getMessage());
56 }
57
58 System.out.println();
59
60 // List is safer — get(index) throws IndexOutOfBoundsException with better message
61 System.out.println("=== List vs array bounds check ===");
62 List<Integer> list = List.of(85, 92, 78, 95, 88);
63 try {
64 list.get(10);
65 } catch (IndexOutOfBoundsException e) {
66 System.out.println("List bounds: " + e.getMessage());
67 }
68 }
69}Output:
=== Safe array operations ===
Sum : 438
Last : 88
Length : 5
Index 0 : 85
Index 4 : 88
=== Boundary violation ===
Caught: Index 5 out of bounds for length 5
Caught: Index -1 out of bounds for length 5
=== List vs array bounds check ===
List bounds: Index: 10, Size: 5
ClassCastException
Thrown when an object is cast to a type it is not an instance of. The instanceof operator checks this at runtime and returns false before a cast that would fail.
1// File: ClassCastDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class ClassCastDemo {
7
8 sealed interface Shape permits Circle, Rectangle {}
9 record Circle(double radius) implements Shape {}
10 record Rectangle(double width, double height) implements Shape {}
11
12 // WRONG — assumes all shapes are Circles
13 static double getRadiusUnsafe(Shape shape) {
14 return ((Circle) shape).radius(); // ClassCastException if shape is Rectangle
15 }
16
17 // CORRECT — instanceof check before cast
18 static double getRadiusSafe(Shape shape) {
19 if (shape instanceof Circle circle) {
20 return circle.radius(); // Java 16+ pattern matching — no explicit cast
21 }
22 return 0.0; // or throw a domain exception
23 }
24
25 // The raw type trap — pre-generics code or suppressed warnings
26 static void demonstrateRawTypeCCE() {
27 List rawList = new ArrayList(); // raw type — no generic type safety
28 rawList.add("hello");
29 rawList.add(42); // different types allowed in raw list
30
31 try {
32 for (Object obj : rawList) {
33 String s = (String) obj; // ClassCastException when Integer is cast to String
34 System.out.println("Value: " + s);
35 }
36 } catch (ClassCastException cce) {
37 System.out.println("ClassCastException from raw list: " +
38 cce.getMessage());
39 }
40 }
41
42 public static void main(String[] args) {
43
44 Shape circle = new Circle(5.0);
45 Shape rectangle = new Rectangle(4.0, 6.0);
46
47 System.out.println("=== instanceof before cast ===");
48 System.out.printf("Circle radius : %.1f%n", getRadiusSafe(circle));
49 System.out.printf("Rectangle radius : %.1f (not a circle)%n", getRadiusSafe(rectangle));
50
51 System.out.println();
52
53 System.out.println("=== Unsafe cast caught ===");
54 try {
55 double radius = getRadiusUnsafe(rectangle);
56 } catch (ClassCastException cce) {
57 System.out.println("ClassCastException: " + cce.getMessage());
58 }
59
60 System.out.println();
61
62 System.out.println("=== Raw type CCE ===");
63 demonstrateRawTypeCCE();
64 }
65}Output:
=== instanceof before cast ===
Circle radius : 5.0
Rectangle radius : 0.0 (not a circle)
=== Unsafe cast caught ===
ClassCastException: class ClassCastDemo$Rectangle cannot be cast to class ClassCastDemo$Circle
=== Raw type CCE ===
Value: hello
ClassCastException from raw list: class java.lang.Integer cannot be cast to class java.lang.String
NumberFormatException
Thrown when a string cannot be parsed into a numeric type because it contains characters that are not valid for the target format.
1// File: NumberFormatDemo.java
2
3import java.util.Optional;
4import java.util.OptionalInt;
5
6public class NumberFormatDemo {
7
8 // WRONG — throws if input is not a valid integer
9 static int parseUnsafe(String input) {
10 return Integer.parseInt(input); // NumberFormatException for non-numeric strings
11 }
12
13 // CORRECT — validate before parse or wrap with try-catch
14 static OptionalInt parseSafe(String input) {
15 if (input == null || input.isBlank()) return OptionalInt.empty();
16 try {
17 return OptionalInt.of(Integer.parseInt(input.trim()));
18 } catch (NumberFormatException nfe) {
19 return OptionalInt.empty();
20 }
21 }
22
23 // Common sources of NumberFormatException
24 static void demonstrateVariants() {
25 String[] inputs = {"42", "3.14", " 100 ", "1,000", "0xFF", "-99", ""};
26
27 for (String input : inputs) {
28 try {
29 int value = Integer.parseInt(input.trim());
30 System.out.printf(" %-10s → %d (OK)%n", "\"" + input + "\"", value);
31 } catch (NumberFormatException nfe) {
32 System.out.printf(" %-10s → NFE: %s%n",
33 "\"" + input + "\"", nfe.getMessage());
34 }
35 }
36
37 System.out.println();
38 // Double.parseDouble handles decimals — use the right parser for the type
39 System.out.println("Double.parseDouble(\"3.14\") = " +
40 Double.parseDouble("3.14"));
41 System.out.println("Integer.decode(\"0xFF\") = " +
42 Integer.decode("0xFF")); // hex — use decode(), not parseInt()
43 }
44
45 public static void main(String[] args) {
46 System.out.println("=== Parse variants ===");
47 demonstrateVariants();
48
49 System.out.println("=== Safe parsing with OptionalInt ===");
50 String[] userInputs = {"150", "abc", null, " 75 ", "9.9"};
51 for (String input : userInputs) {
52 OptionalInt result = parseSafe(input);
53 System.out.printf(" %-8s → %s%n",
54 input,
55 result.isPresent() ? result.getAsInt() : "invalid");
56 }
57 }
58}Output:
=== Parse variants ===
"42" → 42 (OK)
"3.14" → NFE: For input string: "3.14"
" 100 " → 100 (OK)
"1,000" → NFE: For input string: "1,000"
"0xFF" → NFE: For input string: "0xFF"
"-99" → -99 (OK)
"" → NFE: For input string: ""
Double.parseDouble("3.14") = 3.14
Integer.decode("0xFF") = 255
=== Safe parsing with OptionalInt ===
150 → 150
abc → invalid
null → invalid
75 → 75
9.9 → invalid
UnsupportedOperationException and ConcurrentModificationException
These two exceptions share a theme: they both occur when code tries to modify a collection in a way that is not allowed. UnsupportedOperationException fires when mutation is attempted on an immutable or fixed-size view. ConcurrentModificationException fires when a collection is structurally modified during a for-each iteration.
1// File: CollectionExceptionsDemo.java
2
3import java.util.ArrayList;
4import java.util.Arrays;
5import java.util.ConcurrentModificationException;
6import java.util.Iterator;
7import java.util.List;
8
9public class CollectionExceptionsDemo {
10
11 public static void main(String[] args) {
12
13 // ---- UnsupportedOperationException ----
14 System.out.println("=== UnsupportedOperationException sources ===");
15
16 // Source 1: List.of() is fully immutable
17 List<String> immutable = List.of("java", "spring", "kafka");
18 try {
19 immutable.add("redis");
20 } catch (UnsupportedOperationException uoe) {
21 System.out.println("List.of().add() → UnsupportedOperationException");
22 }
23
24 // Source 2: Arrays.asList() is fixed-size — set() works, add/remove do not
25 List<String> fixedSize = Arrays.asList("a", "b", "c");
26 fixedSize.set(0, "x"); // set() allowed — no structural change
27 try {
28 fixedSize.add("d");
29 } catch (UnsupportedOperationException uoe) {
30 System.out.println("Arrays.asList().add() → UnsupportedOperationException");
31 }
32 System.out.println("Arrays.asList after set: " + fixedSize);
33
34 // Source 3: Collections.unmodifiableList() — read-only view
35 List<String> mutable = new ArrayList<>(List.of("one", "two", "three"));
36 List<String> readOnly = java.util.Collections.unmodifiableList(mutable);
37 try {
38 readOnly.add("four");
39 } catch (UnsupportedOperationException uoe) {
40 System.out.println("unmodifiableList.add() → UnsupportedOperationException");
41 }
42
43 System.out.println();
44
45 // ---- ConcurrentModificationException ----
46 System.out.println("=== ConcurrentModificationException sources ===");
47 List<String> services = new ArrayList<>(
48 List.of("auth", "payment", "cart", "notification"));
49
50 // WRONG — list.remove() inside for-each
51 try {
52 for (String service : services) {
53 if ("cart".equals(service)) {
54 services.remove(service); // structural change during iteration → CME
55 }
56 }
57 } catch (ConcurrentModificationException cme) {
58 System.out.println("list.remove() in for-each → ConcurrentModificationException");
59 }
60 System.out.println("List after failed removal attempt: " + services);
61
62 System.out.println();
63
64 System.out.println("=== Safe removal patterns ===");
65 List<String> services2 = new ArrayList<>(
66 List.of("auth", "payment", "cart", "notification"));
67
68 // Pattern 1: removeIf — cleanest single-pass removal
69 services2.removeIf(s -> "cart".equals(s) || "notification".equals(s));
70 System.out.println("After removeIf: " + services2);
71
72 // Pattern 2: Iterator.remove() — safe when conditional removal during iteration
73 List<String> services3 = new ArrayList<>(
74 List.of("auth", "payment", "cart", "notification"));
75 Iterator<String> it = services3.iterator();
76 while (it.hasNext()) {
77 if (it.next().startsWith("c")) {
78 it.remove(); // iterator.remove() — safe
79 }
80 }
81 System.out.println("After iterator.remove(): " + services3);
82 }
83}Output:
=== UnsupportedOperationException sources ===
List.of().add() → UnsupportedOperationException
Arrays.asList().add() → UnsupportedOperationException
Arrays.asList after set: [x, b, c]
unmodifiableList.add() → UnsupportedOperationException
=== ConcurrentModificationException sources ===
list.remove() in for-each → ConcurrentModificationException
List after failed removal attempt: [auth, payment, notification]
=== Safe removal patterns ===
After removeIf: [auth, payment]
After iterator.remove(): [auth, payment, notification]
IllegalArgumentException and IllegalStateException
These two often get confused. IllegalArgumentException is about the arguments passed in — the caller supplied something invalid. IllegalStateException is about the object's state — the caller invoked a method at the wrong time in the object's lifecycle.
1// File: IllegalExceptionsDemo.java
2
3public class IllegalExceptionsDemo {
4
5 // IllegalArgumentException: the argument violates the method's contract
6 static double divide(double numerator, double denominator) {
7 if (denominator == 0.0) {
8 throw new IllegalArgumentException(
9 "Denominator cannot be zero");
10 }
11 return numerator / denominator;
12 }
13
14 // IllegalStateException: the object is not ready for this operation
15 static class DatabaseConnection {
16 private boolean open = false;
17 private String url;
18
19 void connect(String url) {
20 if (open) throw new IllegalStateException(
21 "Already connected — call disconnect() first");
22 this.url = url;
23 this.open = true;
24 System.out.println("Connected to: " + url);
25 }
26
27 String query(String sql) {
28 if (!open) throw new IllegalStateException(
29 "Not connected — call connect() first");
30 return "result-of[" + sql + "]-from-" + url;
31 }
32
33 void disconnect() {
34 if (!open) throw new IllegalStateException("Not connected");
35 this.open = false;
36 System.out.println("Disconnected from: " + url);
37 }
38 }
39
40 public static void main(String[] args) {
41
42 System.out.println("=== IllegalArgumentException ===");
43 System.out.printf("10 / 4 = %.2f%n", divide(10, 4));
44 try {
45 divide(10, 0);
46 } catch (IllegalArgumentException iae) {
47 System.out.println("IllegalArgumentException: " + iae.getMessage());
48 }
49
50 System.out.println();
51
52 System.out.println("=== IllegalStateException ===");
53 DatabaseConnection conn = new DatabaseConnection();
54 try {
55 conn.query("SELECT 1"); // not connected yet
56 } catch (IllegalStateException ise) {
57 System.out.println("IllegalStateException: " + ise.getMessage());
58 }
59
60 conn.connect("jdbc:postgresql://localhost/orders");
61 System.out.println(conn.query("SELECT COUNT(*) FROM orders"));
62
63 try {
64 conn.connect("jdbc:mysql://localhost/backup"); // already connected
65 } catch (IllegalStateException ise) {
66 System.out.println("IllegalStateException: " + ise.getMessage());
67 }
68
69 conn.disconnect();
70 }
71}Output:
=== IllegalArgumentException ===
10 / 4 = 2.50
IllegalArgumentException: Denominator cannot be zero
=== IllegalStateException ===
IllegalStateException: Not connected — call connect() first
Connected to: jdbc:postgresql://localhost/orders
result-of[SELECT COUNT(*) FROM orders]-from-jdbc:postgresql://localhost/orders
IllegalStateException: Already connected — call disconnect() first
Disconnected from: jdbc:postgresql://localhost/orders
ArithmeticException and StackOverflowError
ArithmeticException is thrown specifically by integer division and modulo by zero — int result = 10 / 0. Note that floating-point division by zero does not throw; it returns Infinity or NaN. StackOverflowError is thrown when recursive calls exceed the JVM stack depth limit.
1// File: ArithmeticStackDemo.java
2
3public class ArithmeticStackDemo {
4
5 public static void main(String[] args) {
6
7 // ArithmeticException — only for INTEGER division by zero
8 System.out.println("=== ArithmeticException ===");
9 try {
10 int result = 100 / 0; // integer division by zero → ArithmeticException
11 } catch (ArithmeticException ae) {
12 System.out.println("int 100/0 → ArithmeticException: " + ae.getMessage());
13 }
14
15 // Double division by zero does NOT throw — returns Infinity
16 double infiniteResult = 100.0 / 0.0;
17 System.out.println("double 100.0/0.0 → " + infiniteResult);
18 System.out.println("double 0.0/0.0 → " + (0.0 / 0.0)); // NaN
19
20 System.out.println();
21
22 // StackOverflowError — infinite recursion
23 System.out.println("=== StackOverflowError ===");
24
25 // WRONG: no base case
26 // static int badCount(int n) { return badCount(n + 1); } → SOE
27
28 // CORRECT: base case terminates recursion
29 System.out.println("countdown(5): " + countdown(5));
30
31 try {
32 endlessRecursion(0); // no base case — SOE
33 } catch (StackOverflowError soe) {
34 System.out.println("StackOverflowError caught — infinite recursion detected");
35 }
36 }
37
38 static String countdown(int n) {
39 if (n <= 0) return "launch!"; // base case — recursion terminates
40 return n + "... " + countdown(n - 1);
41 }
42
43 static void endlessRecursion(int depth) {
44 endlessRecursion(depth + 1); // never reaches base case
45 }
46}Output:
=== ArithmeticException ===
int 100/0 → ArithmeticException: / by zero
double 100.0/0.0 → Infinity
double 0.0/0.0 → NaN
=== StackOverflowError ===
countdown(5): 5... 4... 3... 2... 1... launch!
StackOverflowError caught — infinite recursion detected
Real-World Example — Swiggy Order Validation Service
A Swiggy order validation service handles multiple failure scenarios as orders arrive. Each exception type signals a different problem and is handled differently. The service demonstrates production-pattern exception handling: specific catches for each failure mode, IllegalArgumentException for bad input, UnsupportedOperationException awareness for collection views, and controlled exception surfacing to the API layer.
1// File: OrderRequest.java
2
3import java.util.List;
4
5public record OrderRequest(
6 String orderId,
7 String customerId,
8 String restaurantId,
9 List<String> itemIds,
10 double totalAmount,
11 String deliveryAddress) {}1// File: OrderValidationException.java
2
3public class OrderValidationException extends RuntimeException {
4
5 private final String field;
6 private final String code;
7
8 public OrderValidationException(String field, String code, String message) {
9 super(message);
10 this.field = field;
11 this.code = code;
12 }
13
14 public String getField() { return field; }
15 public String getCode() { return code; }
16}1// File: OrderValidationService.java
2
3import java.util.ArrayList;
4import java.util.List;
5import java.util.Map;
6
7public class OrderValidationService {
8
9 private static final Map<String, Double> MENU_PRICES = Map.of(
10 "ITEM-001", 149.0,
11 "ITEM-002", 249.0,
12 "ITEM-003", 89.0
13 );
14
15 // Returns a validated, enriched list — throws specific exceptions per failure
16 public List<String> validateAndEnrich(OrderRequest request) {
17
18 // NullPointerException prevention — validate required fields explicitly
19 if (request == null) {
20 throw new IllegalArgumentException("OrderRequest must not be null");
21 }
22 if (request.customerId() == null || request.customerId().isBlank()) {
23 throw new OrderValidationException("customerId", "MISSING_FIELD",
24 "Customer ID is required");
25 }
26 if (request.itemIds() == null || request.itemIds().isEmpty()) {
27 throw new OrderValidationException("itemIds", "EMPTY_ITEMS",
28 "Order must contain at least one item");
29 }
30
31 // NumberFormatException prevention — parse amounts safely
32 double declaredAmount = request.totalAmount();
33 if (declaredAmount <= 0) {
34 throw new OrderValidationException("totalAmount", "INVALID_AMOUNT",
35 "Total amount must be positive: " + declaredAmount);
36 }
37
38 // UnsupportedOperationException prevention — never mutate a view list
39 // request.itemIds() might be an unmodifiable list — copy before modifying
40 List<String> enrichedItems = new ArrayList<>(request.itemIds());
41
42 // Validate each item exists in the menu (ClassCastException safe via generics)
43 double calculatedTotal = 0.0;
44 for (String itemId : enrichedItems) {
45 if (!MENU_PRICES.containsKey(itemId)) {
46 throw new OrderValidationException("itemIds", "ITEM_NOT_FOUND",
47 "Item not found in restaurant menu: " + itemId);
48 }
49 calculatedTotal += MENU_PRICES.get(itemId);
50 }
51
52 // Business validation — declared total must match calculated total
53 double tolerance = 0.01;
54 if (Math.abs(calculatedTotal - declaredAmount) > tolerance) {
55 throw new OrderValidationException("totalAmount", "AMOUNT_MISMATCH",
56 String.format("Declared Rs.%.2f does not match calculated Rs.%.2f",
57 declaredAmount, calculatedTotal));
58 }
59
60 enrichedItems.add("DELIVERY_FEE"); // safe — working on our own ArrayList copy
61 return enrichedItems;
62 }
63
64 public void processOrder(OrderRequest request) {
65 System.out.printf("Validating order %s from customer %s...%n",
66 request != null ? request.orderId() : "null",
67 request != null ? request.customerId() : "null");
68 try {
69 List<String> enriched = validateAndEnrich(request);
70 System.out.println(" Validation passed. Items: " + enriched);
71
72 } catch (OrderValidationException ove) {
73 System.out.printf(" Validation failed [%s.%s]: %s%n",
74 ove.getField(), ove.getCode(), ove.getMessage());
75
76 } catch (IllegalArgumentException iae) {
77 System.out.println(" Bad request: " + iae.getMessage());
78
79 } catch (Exception unexpected) {
80 System.out.println(" Unexpected error — logged: " +
81 unexpected.getClass().getSimpleName() + ": " + unexpected.getMessage());
82 }
83 }
84
85 public static void main(String[] args) {
86 OrderValidationService service = new OrderValidationService();
87
88 service.processOrder(new OrderRequest(
89 "ORD-001", "C-priya", "REST-101",
90 List.of("ITEM-001","ITEM-002"), 398.0, "HSR Layout, Bengaluru"));
91
92 System.out.println();
93
94 service.processOrder(new OrderRequest(
95 "ORD-002", null, "REST-101",
96 List.of("ITEM-001"), 149.0, "Koramangala"));
97
98 System.out.println();
99
100 service.processOrder(new OrderRequest(
101 "ORD-003", "C-rohan", "REST-101",
102 List.of("ITEM-001","ITEM-999"), 239.0, "Indiranagar"));
103
104 System.out.println();
105
106 service.processOrder(new OrderRequest(
107 "ORD-004", "C-ananya", "REST-101",
108 List.of("ITEM-002","ITEM-003"), 500.0, "Whitefield"));
109 }
110}Output:
Validating order ORD-001 from customer C-priya...
Validation passed. Items: [ITEM-001, ITEM-002, DELIVERY_FEE]
Validating order ORD-002 from customer null...
Validation failed [customerId.MISSING_FIELD]: Customer ID is required
Validating order ORD-003 from customer C-rohan...
Validation failed [itemIds.ITEM_NOT_FOUND]: Item not found in restaurant menu: ITEM-999
Validating order ORD-004 from customer C-ananya...
Validation failed [totalAmount.AMOUNT_MISMATCH]: Declared Rs.500.00 does not match calculated Rs.338.00
Performance Considerations
All exception objects carry the same construction cost: the JVM captures the full stack trace at the moment new SomeException(...) executes. This cost is the same regardless of exception type — NullPointerException and IOException pay the same price. Three practical implications:
1. Do not use exceptions for control flow in loops. Each thrown exception allocates a stack trace object. Testing "is this value in the map?" with containsKey() costs O(1). Catching a hypothetical exception costs microseconds per call. 2. NumberFormatException in a parsing loop is a red flag. If user input is expected to be sometimes invalid, validate first. Integer.parseInt on untrusted input inside a hot path is slow. Use a regex or character-level check before calling parseInt. 3. NullPointerException with helpful messages (Java 14+) has no extra cost. The detailed message is lazily computed — only when getMessage() is called. There is no performance penalty for the improved diagnostics.
Best Practices
Always validate method inputs at the entry point and throw IllegalArgumentException with a clear message. if (userId == null || userId.isBlank()) throw new IllegalArgumentException("userId must not be blank") is the right pattern. This surfaces the error at the point of the call rather than as a cryptic NPE somewhere deep inside the method body. IDEs and static analysis tools like SonarQube flag missing null checks — treat those warnings as real issues.
Use Optional for values that may legitimately be absent rather than returning null. Optional.ofNullable(map.get(key)) makes the absent case explicit in the return type. Callers must use orElse(), orElseGet(), or orElseThrow(), which prevents NPEs by construction. This is appropriate for return types — do not use Optional as a method parameter type.
Use removeIf() instead of removing elements during a for-each loop. list.removeIf(predicate) is the Java 8+ solution to ConcurrentModificationException from structural modification during iteration. It performs a single O(n) pass internally. For cases where Iterator.remove() is needed (more complex removal logic), use an explicit iterator.
Know the three sources of UnsupportedOperationException on lists. List.of() is fully immutable, Arrays.asList() is fixed-size (set() works, add/remove do not), and Collections.unmodifiableList() is a read-only view. Always copy before mutating: new ArrayList<>(existingList). During code reviews, checking that code does not mutate lists coming from API responses or record fields is a common senior developer habit.
Common Mistakes
Mistake 1 — Catching Broad Exception to Hide Specific Failures
1// WRONG — catches everything including NPE, CCE, and more serious failures
2public String getProductName(String id) {
3 try {
4 return catalog.findById(id).getName();
5 } catch (Exception e) {
6 return "Unknown"; // hides NPE if catalog is null, CCE if cast is wrong
7 }
8}
9
10// CORRECT — handle specific exceptions, let others propagate
11public String getProductName(String id) {
12 if (id == null || id.isBlank()) {
13 throw new IllegalArgumentException("Product ID required");
14 }
15 Product product = catalog.findById(id); // propagate NPE if catalog is misconfigured
16 return product != null ? product.getName() : "Unavailable";
17}Mistake 2 — Using Arrays.asList() and Then Calling add()
1// WRONG — Arrays.asList() returns a fixed-size list
2// add() throws UnsupportedOperationException — this is a common beginner mistake
3List<String> tags = Arrays.asList("java", "spring", "kafka");
4tags.add("docker"); // UnsupportedOperationException at runtime
5
6// CORRECT — wrap in ArrayList for a resizable copy
7List<String> tags = new ArrayList<>(Arrays.asList("java", "spring", "kafka"));
8tags.add("docker"); // works fine
9System.out.println(tags); // [java, spring, kafka, docker]Mistake 3 — Trusting Map.get() Without Null Check Before Unboxing
1Map<String, Integer> retryCount = new HashMap<>();
2retryCount.put("ORD-001", 3);
3
4// WRONG — Map.get() returns null for absent keys; unboxing null throws NPE
5int count = retryCount.get("ORD-002"); // NullPointerException
6
7// CORRECT — use getOrDefault() for primitive values
8int count = retryCount.getOrDefault("ORD-002", 0); // returns 0 if absentMistake 4 — Misidentifying the Root Cause When CCE Occurs Mid-Collection
1// WRONG — modifying a shared map while iterating its values
2Map<String, Integer> scores = new HashMap<>(Map.of("A",1,"B",2,"C",3));
3for (Map.Entry<String, Integer> entry : scores.entrySet()) {
4 if (entry.getValue() < 2) {
5 scores.remove(entry.getKey()); // ConcurrentModificationException
6 }
7}
8
9// CORRECT — use removeIf on entrySet
10scores.entrySet().removeIf(entry -> entry.getValue() < 2);
11System.out.println(scores); // {B=2, C=3}Output:
{B=2, C=3}
Interview Questions
Q1. What is NullPointerException and what are the common ways to prevent it?
NullPointerException is thrown when code dereferences a null reference — calling a method, accessing a field, or unboxing a null Integer to int. Prevention strategies: validate method arguments at entry points with explicit null checks and throw IllegalArgumentException; use Optional<T> for return types that may legitimately be absent; use getOrDefault() instead of get() on maps when unboxing; and in Java 14+, NPE messages include the exact variable name that was null, making debugging much faster.
Q2. What causes ConcurrentModificationException and how do you fix it?
ConcurrentModificationException is thrown when a collection's structural modification count (modCount) changes between an iterator's creation and its next hasNext() or next() call. The most common trigger: calling list.remove() or list.add() inside a for-each loop. The for-each loop creates an iterator at the start; list.remove() increments modCount; the iterator detects the mismatch on the next call. Fix: use list.removeIf(predicate) for conditional removal (cleanest), iterator.remove() inside an explicit iterator loop, or collect the items to remove first and then call removeAll.
Q3. What is the difference between IllegalArgumentException and IllegalStateException?
IllegalArgumentException is about the arguments a caller passed — the caller provided a value that violates the method's documented contract: null where non-null is required, a negative number where a positive was expected. The caller needs to fix the argument. IllegalStateException is about the state of the object at the time of the call — the method was invoked in the wrong order or on an object not yet ready: calling query() before connect(), accessing a closed stream, using an iterator that has been exhausted. The caller needs to fix the call sequence.
Q4. Why does double division by zero not throw ArithmeticException in Java?
Java follows IEEE 754 floating-point arithmetic for double and float. Division by zero is a defined operation in this standard — it produces Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, or Double.NaN depending on the operands. ArithmeticException is thrown only for integer and long division/modulo by zero, because integer arithmetic has no representation for infinity. This distinction appears in interview questions to test whether the candidate knows that 1.0 / 0.0 returns Infinity while 1 / 0 throws.
Q5. What causes UnsupportedOperationException when working with lists?
Three common sources: List.of() creates a fully immutable list — no add, remove, or set; Arrays.asList() creates a fixed-size list backed by the array — set() works (writes through to the array) but add() and remove() throw because they would change the size; and Collections.unmodifiableList() creates a read-only view where all mutating operations throw. The fix in all three cases: copy into a new ArrayList before mutating: new ArrayList<>(existingList).
Q6. What is ClassCastException and when does it occur?
ClassCastException is thrown when an object is cast to a type it is not an instance of — when object instanceof TargetType would return false. Common causes: casting an object from a raw-type collection (pre-generics code), incorrectly narrowing a supertype reference without checking with instanceof first, and deserialization producing the wrong type. Prevention: always use instanceof before casting, use generics to avoid raw types, and in modern Java use pattern matching (if (shape instanceof Circle c) { c.radius(); }) which combines the check and cast in one expression.
FAQs
Which Java exception is most common in production?
NullPointerException is consistently the most frequently encountered exception in Java applications. Studies of production Java logs across large codebases place it as the leading exception by volume. Java 14's helpful NPE messages — which name the null variable directly — significantly reduced the debugging time for NPEs in modern codebases.
Can ArithmeticException occur with floating-point division?
No. Floating-point division by zero follows IEEE 754 and returns Infinity or NaN — it never throws. ArithmeticException is exclusive to integer and long division and modulo operations (5 / 0 or 5 % 0). To check for problematic floating-point results use Double.isInfinite(value) and Double.isNaN(value).
Why does Arrays.asList() allow set() but not add() or remove()?
Arrays.asList() returns a List backed by the original array. Arrays have a fixed size — you can replace an element at an existing index, but you cannot add more slots or remove slots. set() changes the value at an existing index, which is valid — it writes through to the backing array. add() and remove() would change the length of the list, which is structurally impossible for an array-backed list of fixed size.
What is the difference between IndexOutOfBoundsException and ArrayIndexOutOfBoundsException?
ArrayIndexOutOfBoundsException is a subclass of IndexOutOfBoundsException. ArrayIndexOutOfBoundsException is thrown specifically by array access operations when the index is out of range. IndexOutOfBoundsException (or its other subclass StringIndexOutOfBoundsException) is thrown by List.get(), String.charAt(), and similar indexed operations. When catching bounds errors generically, catch IndexOutOfBoundsException — it handles all index-based access failures.
What is a helpful NullPointerException and which Java version introduced it?
Java 14 introduced detailed NPE messages that describe exactly which part of an expression was null. For example: Cannot invoke "String.toUpperCase()" because the return value of "UserProfile.address()" is null tells you address() returned null — not just that a NPE occurred somewhere. This feature is enabled by default in Java 14 and eliminates much of the guesswork when an NPE occurs in a chained expression.
How do I read a Java stack trace to find which exception occurred and where?
Read the stack trace from top to bottom. The first line names the exception class and its message — java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null. The lines below (beginning with at) are the call frames from where the exception was thrown down to where execution started. The top frame (at com.example.OrderService.validate(OrderService.java:42)) is where the exception originated — start debugging there. If the exception was wrapped, look for Caused by: further down — that shows the root cause thrown before the wrapper.
Summary
The common Java exceptions form a recognisable vocabulary. NullPointerException — null dereference, prevented with null checks, Optional, and getOrDefault. ArrayIndexOutOfBoundsException — off-by-one in index math, prevented with for-each. ClassCastException — invalid cast, prevented with instanceof before casting. NumberFormatException — non-numeric string, prevented by validating before parsing. ConcurrentModificationException — structural change during iteration, fixed by removeIf or iterator.remove(). UnsupportedOperationException — mutating an immutable or fixed-size view, fixed by copying first.
For interviews, know the root cause of each, know the one-line prevention for each, and know which ones are unchecked (all of the above) versus checked (IOException, SQLException). The question "what causes ConcurrentModificationException" is among the most frequently asked in Java collections interviews at both service and product companies.
What to Read Next
| Topic | Link |
|---|---|
| The complete exception handling foundation — try, catch, finally, throw, and throws | Java Exception Handling → |
| How try, catch, and finally work together with all sequencing rules | Java try-catch → |
| The difference between checked and unchecked exceptions with design guidance | Java Checked vs Unchecked Exceptions → |
| How to design custom exception classes for your domain with the right fields | Java Custom Exceptions → |
| How throw and throws differ and how the declaration propagates through method signatures | Java throw and throws → |