Java Unchecked Exceptions

Su Suriya Ravichandran Updated 14 Nov 2026
10 min read ·Lesson 130 of 133

What Makes an Exception "Unchecked"

An unchecked exception is one the compiler does not force you to catch or declare. In Java this means:

  • RuntimeException and all its subclasses, and
  • Error and all its subclasses.
Throwable
 ├── Error                              → unchecked
 │    ├── OutOfMemoryError
 │    └── StackOverflowError
 └── Exception
      └── RuntimeException              → unchecked
           ├── ArithmeticException
           ├── NullPointerException
           ├── ClassCastException
           ├── IllegalArgumentException
           │    └── NumberFormatException
           ├── IllegalStateException
           ├── IndexOutOfBoundsException
           ├── UnsupportedOperationException
           └── ConcurrentModificationException

Like checked exceptions, unchecked exceptions are thrown, propagate and can be caught in exactly the same way. The only difference is at compile time: methods can throw them without a throws clause, and callers can call those methods without try...catch.

Why the Compiler Doesn't Force Handling

Unchecked exceptions mostly represent programming errors: dividing by a value that should never be zero, using a null reference, reading past the end of an array, calling a method with an argument that breaks its contract. Almost any line of Java could cause one:

  • Every method call on an object can throw NullPointerException.
  • Every array access can throw ArrayIndexOutOfBoundsException.
  • Every integer division can throw ArithmeticException.

If these were checked, nearly every method would need try...catch blocks or long throws clauses, and the important declarations (like IOException) would be lost in noise. More importantly, catching them rarely helps: the correct response to a NullPointerException caused by a bug is to fix the bug, not to write recovery code for it.

So the Java designers split exceptions by who can do something about them:

CheckedUnchecked
Base classException (not RuntimeException)RuntimeException, Error
Compiler enforcementMust be caught or declaredNot enforced
Typically representsExpected external failures (I/O, network, database)Bugs and contract violations
Who can fix itCode handles it at runtime (retry, fallback, message)A developer fixes the code
ExamplesIOException, SQLException, InterruptedExceptionNullPointerException, IllegalArgumentException, ArithmeticException
In the method signatureListed in throwsUsually documented with Javadoc @throws only

Propagation Without Declarations

public class NoDecl {
    static int unitPrice(int total, int qty) {
        return total / qty;                       // may throw ArithmeticException
    }

    static int parseQty(String text) {
        return Integer.parseInt(text);            // may throw NumberFormatException
    }

    public static void main(String[] args) {
        System.out.println(unitPrice(1200, parseQty("4")));
        System.out.println(unitPrice(1200, parseQty("0")));
    }
}

Output:

300
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at NoDecl.unitPrice(NoDecl.java:3)
	at NoDecl.main(NoDecl.java:12)

Neither method has a throws clause and main has no try, yet the program compiles. When qty is 0, the exception propagates from unitPrice to main and ends the program, exactly like a checked exception would. "Unchecked" doesn't mean "harmless" — it means the compiler won't remind you.

This is also the main risk: nothing in unitPrice's signature warns callers that it can fail. Document such behaviour with Javadoc, and validate inputs so the failure is clear:

/**
 * @throws IllegalArgumentException if qty is not positive
 */
static int unitPrice(int total, int qty) {
    if (qty <= 0) {
        throw new IllegalArgumentException("qty must be positive, got " + qty);
    }
    return total / qty;
}

IllegalArgumentException with a specific message tells the caller exactly what they did wrong, while / by zero only describes the symptom.

Unchecked Exceptions That Protect You

Several JDK methods throw unchecked exceptions on purpose, to turn silent bugs into visible failures. This approach is called fail-fast: stop immediately at the point where something is wrong, instead of continuing with corrupt data.

Integer overflow

public class Overflow {
    public static void main(String[] args) {
        int stock = Integer.MAX_VALUE;
        System.out.println("plain +:   " + (stock + 1));
        try {
            System.out.println("addExact: " + Math.addExact(stock, 1));
        } catch (ArithmeticException e) {
            System.out.println("addExact:  ArithmeticException: " + e.getMessage());
        }
        try {
            System.out.println(Math.toIntExact(3_000_000_000L));
        } catch (ArithmeticException e) {
            System.out.println("toIntExact: " + e.getMessage());
        }
    }
}

Output:

plain +:   -2147483648
addExact:  ArithmeticException: integer overflow
toIntExact: integer overflow

Plain + silently wraps around: adding 1 to the largest int produces the most negative int. In an inventory or banking system that would be a disaster that nobody notices until the reports are wrong. Math.addExact, subtractExact, multiplyExact and toIntExact perform the same operations but throw ArithmeticException when the result doesn't fit. Use them wherever an overflow would be a bug, such as money stored in long paise or cents.

Modifying a collection while iterating

import java.util.*;

public class Cme {
    public static void main(String[] args) {
        List<String> cart = new ArrayList<>(List.of("pen", "expired-coupon", "bag", "ink"));
        try {
            for (String item : cart) {
                if (item.startsWith("expired")) {
                    cart.remove(item);
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("ConcurrentModificationException, cart = " + cart);
        }

        List<String> cart2 = new ArrayList<>(List.of("pen", "expired-coupon", "bag", "ink"));
        cart2.removeIf(item -> item.startsWith("expired"));
        System.out.println("removeIf: " + cart2);
    }
}

Output:

ConcurrentModificationException, cart = [pen, bag, ink]
removeIf: [pen, bag, ink]

The enhanced for loop uses an iterator. When cart.remove(item) changes the list behind the iterator's back, the iterator detects the change on its next step and throws ConcurrentModificationException rather than risk skipping or repeating elements. The fix is not to catch the exception but to use the right tool: removeIf, or an explicit Iterator with iterator.remove().

The detection is "best effort": the JDK documentation explicitly says it isn't guaranteed in every case (for example, removing the second-to-last element can end the loop without an exception). Never rely on catching it — it exists to expose a bug.

Errors: The Other Unchecked Family

Error subclasses are also unchecked, but they signal problems with the JVM or the environment rather than with your application logic.

public class Deep {
    static int depth = 0;

    static long factorial(long n) {
        depth++;
        return n * factorial(n - 1);   // bug: no base case
    }

    public static void main(String[] args) {
        try {
            factorial(5);
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError after more than " + (depth / 1000) * 1000 + " calls");
        }
    }
}

Output (the exact number depends on the JVM's thread stack size and settings):

StackOverflowError after more than 7000 calls

Each method call uses some memory on the thread's stack. Without a base case (if (n <= 1) return 1;), the recursion never stops, the stack runs out and the JVM throws StackOverflowError. The example catches it only to show the depth; in real code, catching Errors is almost never correct, because the JVM may be in an unreliable state (especially after OutOfMemoryError). The fix is in the code — here, the missing base case.

Practical Example: Unchecked Exceptions in a Service Layer

Modern Java applications (Spring Boot, Jakarta EE, Quarkus) use unchecked exceptions heavily. Business code throws them wherever a rule is broken, without adding throws clauses everywhere, and one central handler at the boundary translates them into responses. This example imitates that structure with a tiny product reservation service.

import java.util.*;

public class Api {
    static class NotFoundException extends RuntimeException {
        NotFoundException(String message) { super(message); }
    }

    record Product(String sku, String name, int stock) { }

    static final Map<String, Product> catalog = new HashMap<>(Map.of(
        "PEN-01", new Product("PEN-01", "Gel pen", 40),
        "BAG-07", new Product("BAG-07", "Laptop bag", 0)));

    // Service layer: throws unchecked exceptions, no throws clauses needed
    static Product find(String sku) {
        Product p = catalog.get(sku);
        if (p == null) throw new NotFoundException("no product " + sku);
        return p;
    }

    static String reserve(String sku, String qtyText) {
        int qty = Integer.parseInt(qtyText);
        if (qty <= 0) throw new IllegalArgumentException("quantity must be positive: " + qty);
        Product p = find(sku);
        if (p.stock() < qty) throw new IllegalStateException(p.name() + " has only " + p.stock() + " in stock");
        catalog.put(sku, new Product(sku, p.name(), p.stock() - qty));
        return "reserved " + qty + " x " + p.name();
    }

    // Boundary: one place translates exceptions into HTTP-style responses
    static String handle(String sku, String qty) {
        try {
            return "200 " + reserve(sku, qty);
        } catch (RuntimeException e) {
            int status = switch (e) {
                case NotFoundException nf -> 404;
                case IllegalArgumentException iae -> 400;   // also covers NumberFormatException
                case IllegalStateException ise -> 409;
                default -> 500;
            };
            return status + " " + e.getMessage();
        }
    }

    public static void main(String[] args) {
        System.out.println(handle("PEN-01", "5"));
        System.out.println(handle("PEN-01", "two"));
        System.out.println(handle("PEN-01", "-1"));
        System.out.println(handle("INK-03", "1"));
        System.out.println(handle("BAG-07", "1"));
        System.out.println("PEN-01 stock now " + catalog.get("PEN-01").stock());
    }
}

Output:

200 reserved 5 x Gel pen
400 For input string: "two"
400 quantity must be positive: -1
404 no product INK-03
409 Laptop bag has only 0 in stock
PEN-01 stock now 35

How it works:

  1. NotFoundException extends RuntimeException — a custom unchecked exception (the next lesson covers custom exceptions in detail). Because it's unchecked, find and reserve need no throws clause.
  2. Each rule throws the exception that describes it. Non-numeric text makes Integer.parseInt throw NumberFormatException; a non-positive quantity is an invalid argument; a missing product is "not found"; insufficient stock is a state conflict.
  3. The state changes only after every check passes. Only the first request modifies the catalogue, which is why the pen stock ends at 40 - 5 = 35.
  4. handle is the boundary. It catches RuntimeException once and maps each type to a status code using a switch with type patterns (Java 21). Each case Type name -> matches when e is an instance of that type. The order matters as it does for catch blocks: a case for IllegalArgumentException placed above a case for one of its subclasses would make the subclass case unreachable, and the compiler rejects that.
  5. default -> 500 covers everything unexpected, such as a NullPointerException from a real bug. Those become "500 Internal Server Error" and should be logged with their stack trace and fixed.

In Spring Boot, the same idea is implemented with a class annotated @RestControllerAdvice containing @ExceptionHandler methods; the business code stays free of HTTP concerns and of try...catch blocks.

Choosing Between Checked and Unchecked for Your Own Code

When you throw an exception from your own method, use these questions:

QuestionIf yes
Does the problem come from the caller breaking the method's contract (bad argument, wrong order of calls)?Unchecked (IllegalArgumentException, IllegalStateException)
Could the caller have prevented it with a simple check beforehand?Unchecked
Is it a foreseeable condition outside anyone's control that the immediate caller should handle (file missing, connection lost)?Checked
Will the exception be thrown from inside lambdas or streams?Unchecked (or wrap the checked one)
Is the code a framework-style application where one central handler deals with failures?Unchecked is the common convention

Oracle's original guideline summarises it: "If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception." In practice, many modern codebases lean toward unchecked exceptions for business errors and reserve checked exceptions for APIs where the caller genuinely must make a recovery decision.

Catching Unchecked Exceptions: When It Is Appropriate

Not having to catch unchecked exceptions doesn't mean you never should. Catching them is appropriate when:

  • Parsing untrusted input. NumberFormatException, DateTimeParseException and IllegalArgumentException from UUID.fromString or Enum.valueOf are the normal way to detect invalid user input.
  • At application boundaries. Request handlers, message consumers, scheduled jobs and thread-pool tasks catch RuntimeException so one failure doesn't take down the whole process, and log it with the full stack trace.
  • When a library uses unchecked exceptions for expected failures. For example, JPA's NoResultException or Spring's DataIntegrityViolationException for a duplicate key.

Avoid catching NullPointerException, ArrayIndexOutOfBoundsException or ClassCastException as a way to control normal program flow. Prevent them with checks (!= null, bounds checks, instanceof patterns) instead.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Java Unchecked Exceptions and get answers drawn from it.

Signed-in readers only.