Java Exception Basics
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:
"Report started"prints normally.maincallsaverage(scores, 0). Inside it,sum / countperforms integer division by zero, which is mathematically undefined, so the JVM creates anArithmeticExceptionwith the message/ by zeroand throws it.averagehas no code to handle it, so the method ends immediately — it never returns a value. The exception travels back to the caller,main.maindoesn't handle it either, so the exception leavesmain. The thread"main"dies, and the JVM's default handler prints the report. The program exits with a non-zero status code."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)
| Part | Meaning |
|---|---|
Exception in thread "main" | The thread in which the exception was not handled |
java.lang.ArithmeticException | The fully qualified exception class — what kind of problem |
/ by zero | The 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
catchblock (covered in the next lesson), or - it leaves the thread's starting method (
mainor a thread'srun), 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
Error | Exception | |
|---|---|---|
| Meaning | The JVM itself is in trouble | The application hit a problem |
| Examples | OutOfMemoryError, StackOverflowError, NoClassDefFoundError | IOException, NumberFormatException, IllegalStateException |
| Can the program reasonably recover? | Usually not | Often yes — retry, use a default, report to the user |
| Should you catch it? | Generally no | Yes, 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
Exceptionthat are not subclasses ofRuntimeException— for exampleIOException,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
RuntimeExceptionand its subclasses (plus allErrors). They usually indicate a programming bug — anullthat 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
| Exception | Typical cause | Usual fix |
|---|---|---|
ArithmeticException | Integer division or % by zero | Validate the divisor before dividing |
ArrayIndexOutOfBoundsException | Index < 0 or >= array.length | Check bounds; loop with i < array.length |
NumberFormatException | Text that isn't a valid number given to parseInt/parseDouble | Validate or handle user input |
StringIndexOutOfBoundsException | charAt/substring with an invalid index | Check length() first |
ClassCastException | Casting an object to a type it isn't | Use instanceof pattern matching before casting |
UnsupportedOperationException | Modifying an immutable collection such as List.of(...) | Copy into new ArrayList<>(...) if you need to modify it |
NoSuchElementException | next() on an exhausted iterator, Optional.get() on empty | Call hasNext() / use orElse |
NullPointerException | Using a null reference | Initialise values, check for null, use Optional for "may be absent" results |
IllegalArgumentException | A method received an invalid argument | Pass valid values; your own methods should throw it for bad input |
IllegalStateException | A method was called at the wrong time | Respect 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:
- 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. - Bugs —
NullPointerException,ArrayIndexOutOfBoundsException,ClassCastException. These should not be "handled away"; they should be logged with their full stack trace and fixed in the code. - 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.