Java Exception Basics

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

What an Exception Is

An exception is an event that interrupts the normal flow of a program because something went wrong while it was running: dividing by zero, reading an array index that doesn't exist, opening a file that isn't there, or receiving text where a number was expected.

In Java, an exception is also an object. When the problem happens, the JVM (or your own code) creates an object describing it — its type, a message and the exact place in the code where it happened — and throws it. Normal execution stops at that line, and Java looks for code that has declared it can handle that kind of problem.

This design exists for a reason. In older languages such as C, functions report failure through return codes (-1, NULL), and every caller must remember to check them. Forgetting a check silently produces wrong results. Java's exceptions cannot be ignored by accident: if nobody handles one, the thread stops and prints a report. Exceptions also separate the "happy path" code from the error-handling code, which keeps both easier to read.

What Happens When an Exception Is Not Handled

public class Crash {
    public static void main(String[] args) {
        int[] scores = {80, 90, 70};
        System.out.println("Report started");
        System.out.println("Average: " + average(scores, 0));
        System.out.println("Report finished");
    }

    static int average(int[] values, int count) {
        int sum = 0;
        for (int v : values) {
            sum += v;
        }
        return sum / count;
    }
}

Output:

Report started
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Crash.average(Crash.java:14)
	at Crash.main(Crash.java:5)

Here is what happened:

  1. "Report started" prints normally.
  2. main calls average(scores, 0). Inside it, sum / count performs integer division by zero, which is mathematically undefined, so the JVM creates an ArithmeticException with the message / by zero and throws it.
  3. average has no code to handle it, so the method ends immediately — it never returns a value. The exception travels back to the caller, main.
  4. main doesn't handle it either, so the exception leaves main. The thread "main" dies, and the JVM's default handler prints the report. The program exits with a non-zero status code.
  5. "Report finished" is never printed. Every statement after the failing point is skipped.

Note that floating-point division behaves differently: 240.0 / 0 gives Infinity and no exception is thrown. Only integer division and remainder (/, % on int and long) throw ArithmeticException.

Reading a Stack Trace

The printed report is called a stack trace. Learning to read it is the fastest way to find bugs.

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Crash.average(Crash.java:14)
	at Crash.main(Crash.java:5)
PartMeaning
Exception in thread "main"The thread in which the exception was not handled
java.lang.ArithmeticExceptionThe fully qualified exception class — what kind of problem
/ by zeroThe message — details of this occurrence
at Crash.average(Crash.java:14)The top frame: the exact method, file and line where it was thrown
at Crash.main(Crash.java:5)The method that called it, and the line of that call

Read it from the top: the first at line is where the problem occurred; each line below is the caller of the line above. The list mirrors the call stack — the chain of method calls that were active at that moment. In real applications the trace is often dozens of lines long, mixing your code with library code. Look for the first frame that belongs to your package; that is usually where the fix belongs.

When one exception is caused by another, the trace contains a Caused by: section. That is covered in the Exception Chaining lesson.

Exception Propagation

The behaviour in step 3 above has a name: propagation. When a method throws an exception and doesn't handle it, the exception moves up to the method that called it, then to that method's caller, and so on, until either:

  • some method handles it with a catch block (covered in the next lesson), or
  • it leaves the thread's starting method (main or a thread's run), and the thread terminates.

Propagation is what lets you handle an error at the level that actually knows what to do about it. A low-level method that parses a number doesn't know whether the right response is to show a form error, skip a CSV line or reject an API request; the code higher up does.

Helpful NullPointerException Messages

A NullPointerException (NPE) is thrown when code tries to use a reference that points to nothing (null) — calling a method on it, reading a field, or reading its array length.

public class Npe {
    record Address(String city) { }
    record Customer(String name, Address address) { }

    public static void main(String[] args) {
        Customer c = new Customer("Asha", null);
        System.out.println(c.address().city().toUpperCase());
    }
}

Output:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Npe$Address.city()" because the return value of "Npe$Customer.address()" is null
	at Npe.main(Npe.java:7)

Line 7 has three method calls chained together, so the line number alone doesn't tell you which reference was null. Since Java 14 (enabled by default from Java 15), the JVM generates a helpful NPE message that names the exact expression: c.address() returned null, so .city() couldn't be called. Npe$Address is how the JVM names a nested type Address inside class Npe.

On Java 8–13 the same program prints only java.lang.NullPointerException with no message — one reason to run on a modern Java version.

The Exception Class Hierarchy

Every exception and error in Java is a subclass of java.lang.Throwable. Only Throwable objects can be thrown and caught. The hierarchy decides how Java treats each problem.

Object
 └── Throwable
      ├── Error                      (serious JVM problems — don't catch)
      │    ├── OutOfMemoryError
      │    └── StackOverflowError
      └── Exception                  (problems an application may handle)
           ├── IOException            (checked)
           │    └── FileNotFoundException
           ├── SQLException           (checked)
           └── RuntimeException       (unchecked)
                ├── ArithmeticException
                ├── NullPointerException
                ├── IllegalArgumentException
                │    └── NumberFormatException
                └── IndexOutOfBoundsException
                     ├── ArrayIndexOutOfBoundsException
                     └── StringIndexOutOfBoundsException

You can confirm parent chains with reflection:

import java.io.FileNotFoundException;

public class Tree {
    public static void main(String[] args) {
        Class<?>[] types = { FileNotFoundException.class, ArithmeticException.class, OutOfMemoryError.class };
        for (Class<?> t : types) {
            StringBuilder chain = new StringBuilder(t.getSimpleName());
            for (Class<?> p = t.getSuperclass(); p != null; p = p.getSuperclass()) {
                chain.append(" -> ").append(p.getSimpleName());
            }
            System.out.println(chain);
        }
    }
}

Output:

FileNotFoundException -> IOException -> Exception -> Throwable -> Object
ArithmeticException -> RuntimeException -> Exception -> Throwable -> Object
OutOfMemoryError -> VirtualMachineError -> Error -> Throwable -> Object

getSuperclass() returns each class's parent, and the loop climbs until it reaches Object, whose parent is null.

The hierarchy matters because a handler for a parent type also handles all of its subclasses. Handling IOException also handles FileNotFoundException; handling RuntimeException handles NullPointerException, ArithmeticException and every other runtime exception.

Error vs Exception

ErrorException
MeaningThe JVM itself is in troubleThe application hit a problem
ExamplesOutOfMemoryError, StackOverflowError, NoClassDefFoundErrorIOException, NumberFormatException, IllegalStateException
Can the program reasonably recover?Usually notOften yes — retry, use a default, report to the user
Should you catch it?Generally noYes, where you can do something useful

A StackOverflowError, for example, means a method recursed so deeply that the thread ran out of stack memory. Catching it doesn't fix the faulty recursion; the right response is to fix the code.

Checked and Unchecked Exceptions

Java divides exceptions into two groups. The difference is enforced by the compiler, not at runtime.

  • Checked exceptions are subclasses of Exception that are not subclasses of RuntimeException — for example IOException, SQLException, InterruptedException. They represent problems outside the program's control that a correct program must still expect: a file can be deleted, a network can drop. The compiler forces you to either handle them or declare that your method passes them on.
  • Unchecked exceptions are RuntimeException and its subclasses (plus all Errors). They usually indicate a programming bug — a null that should have been checked, a wrong index — so the compiler doesn't force handling. The fix is normally to correct the code, not to wrap it in error handling.

The compiler check looks like this:

import java.nio.file.*;

public class Checked {
    public static void main(String[] args) {
        String text = Files.readString(Path.of("settings.txt"));
        System.out.println(text);
    }
}

Compiling produces:

Checked.java:5: error: unreported exception IOException; must be caught or declared to be thrown
        String text = Files.readString(Path.of("settings.txt"));
                                      ^
1 error

Files.readString declares that it can throw IOException, so the program doesn't compile until the code either catches it (try/catch) or declares it (throws IOException). Both options are explained in the following lessons, and the two categories are covered in depth in the Checked Exceptions and Unchecked Exceptions lessons.

Common Runtime Exceptions

The program below triggers several common unchecked exceptions one after another. It uses a try/catch block (explained in the next lesson) only so that one failure doesn't stop the rest of the demonstration.

import java.util.*;

public class Kinds {
    public static void main(String[] args) {
        List<Runnable> faults = List.of(
            () -> System.out.println(10 / 0),
            () -> { int[] slots = new int[3]; slots[5] = 1; },
            () -> System.out.println(Integer.parseInt("12a")),
            () -> System.out.println("Java".charAt(10)),
            () -> { Object o = "text"; Integer n = (Integer) o; },
            () -> List.of(1, 2).add(3),
            () -> new ArrayList<String>().iterator().next()
        );
        for (Runnable fault : faults) {
            try {
                fault.run();
            } catch (RuntimeException e) {
                System.out.println(e.getClass().getSimpleName() + " -> " + e.getMessage());
            }
        }
    }
}

Output:

ArithmeticException -> / by zero
ArrayIndexOutOfBoundsException -> Index 5 out of bounds for length 3
NumberFormatException -> For input string: "12a"
StringIndexOutOfBoundsException -> Index 10 out of bounds for length 4
ClassCastException -> class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
UnsupportedOperationException -> null
NoSuchElementException -> null
ExceptionTypical causeUsual fix
ArithmeticExceptionInteger division or % by zeroValidate the divisor before dividing
ArrayIndexOutOfBoundsExceptionIndex < 0 or >= array.lengthCheck bounds; loop with i < array.length
NumberFormatExceptionText that isn't a valid number given to parseInt/parseDoubleValidate or handle user input
StringIndexOutOfBoundsExceptioncharAt/substring with an invalid indexCheck length() first
ClassCastExceptionCasting an object to a type it isn'tUse instanceof pattern matching before casting
UnsupportedOperationExceptionModifying an immutable collection such as List.of(...)Copy into new ArrayList<>(...) if you need to modify it
NoSuchElementExceptionnext() on an exhausted iterator, Optional.get() on emptyCall hasNext() / use orElse
NullPointerExceptionUsing a null referenceInitialise values, check for null, use Optional for "may be absent" results
IllegalArgumentExceptionA method received an invalid argumentPass valid values; your own methods should throw it for bad input
IllegalStateExceptionA method was called at the wrong timeRespect the object's lifecycle (e.g. don't use a closed resource)

Notice that two messages are null. The message is optional; not every exception provides one. Code that logs exceptions should print the class name as well as the message, otherwise a log line reading just null gives no clue.

Where Exceptions Fit in a Real Application

In a typical web application or service, exceptions fall into three practical groups:

  1. Expected, recoverable situations — a user types "abc" into an age field, a file an admin uploaded is missing, a remote API times out. These are handled close to where they occur: show a validation message, use a default, retry.
  2. Bugs — NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException. These should not be "handled away"; they should be logged with their full stack trace and fixed in the code.
  3. Fatal conditions — OutOfMemoryError, configuration missing at startup. The application often cannot continue, and failing fast with a clear message is the correct outcome.

Frameworks such as Spring Boot provide a central place (a global exception handler) that turns uncaught exceptions into HTTP error responses, so a single request fails instead of the whole server. The building blocks they use are the same ones covered in the next lessons: try, catch, finally, throw and throws.

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 Exception Basics and get answers drawn from it.

Signed-in readers only.