Exceptions in Java — theory with grades, students and red stack traces (and the “7,5” that brought my whole program down)
My first grades program read the marks as text and worked out the average. I tested it with "8.5", "6" and "9" and it worked perfectly. Then I gave it to a classmate, who typed the grades the way everyone writes them in Spain, with a comma: "7,5". And the console filled up with red:
Exception in thread "main" java.lang.NumberFormatException: For input string: "7,5" at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054) at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.base/java.lang.Double.parseDouble(Double.java:792) at Average.convert(Average.java:4) at Average.average(Average.java:10) at Average.main(Average.java:17)
One badly written grade and the whole program stopped, without working anything out. At the time those lines felt like a punishment. Today I know they’re the most useful thing Java can give you: they tell you what happened, where, and how you got there. In this article we’ll learn to read them and, above all, to stop a weird grade from bringing the program down.
We’ll keep the students from classes and objects and, at the end, we’ll use what you saw in inheritance to create our own exceptions.
Table of Contents
What an exception is
An exception is an object that Java creates when something goes wrong while the program is running: converting "7,5" to a number, accessing position 5 of a 3-element array, calling a method on null… Java “throws” that object and, if nobody catches it, the program stops and prints the stack trace.
This is the program from my story:
public class Average {
static double convert(String text) {
return Double.parseDouble(text.trim());
}
static double average(String[] grades) {
double sum = 0;
for (String g : grades) {
sum += convert(g);
}
return sum / grades.length;
}
public static void main(String[] args) {
String[] grades = {"8.5", "7,5", "6"};
System.out.println("Average: " + average(grades));
}
}
Notice that "Average: " never appears. As soon as the exception is thrown, Java abandons convert, then average and then main, without finishing any of them.
You’ve already met a few: ArrayIndexOutOfBoundsException with arrays, NullPointerException in classes and objects, ClassCastException in inheritance. Until now we avoided them by checking first. Today we learn to handle them when they can’t be avoided.
How to read a stack trace
You read a stack trace like this:
- First line: the exception type and the message.
NumberFormatException: For input string: "7,5"already says almost everything: something tried to convert"7,5"to a number and couldn’t. - **The
at …lines: the call stack, from the most recent (top) to the oldest** (bottom). Each one gives the method, file and line. - Look for your code: lines starting with
java.base/are Java’s internals; you won’t touch them. The first line that’s yours (Average.convert(Average.java:4)) is where the problem happened in your program. - Keep going down to see how you got there:
main(line 17) calledaverage(line 10), which calledconvert(line 4).
Don’t read the whole trace at once. First line (what) → first line of your code (where) → the rest only if you need to know the path. In IntelliJ, the file name and line are a link that takes you straight there.
The line numbers in java.base/… change depending on your Java version. Yours don’t: Average.java:4 is always line 4 of your file.
try and catch: catching the exception
To stop the program from dying, we put the code that might fail in a try block and say what to do if it fails in a catch:
String[] grades = {"8.5", "7,5", "6"};
double sum = 0;
int valid = 0;
for (String g : grades) {
try {
double n = Double.parseDouble(g);
sum += n;
valid++;
System.out.println("OK: " + n);
} catch (NumberFormatException e) {
System.out.println("Skipped -> " + e.getMessage());
}
}
System.out.println("Average of " + valid + " grades: " + sum / valid);
Output:
OK: 8.5 Skipped -> For input string: "7,5" OK: 6.0 Average of 2 grades: 7.25
What runs and what doesn’t
This is the part that’s hardest to get. When a line inside the try throws an exception:
- **The rest of the
tryis skipped.** With"7,5", neithersum += n, norvalid++, nor the “OK”printlnrun. - **Java looks for a
catchthat matches** the exception’s type. Here,NumberFormatException: it matches, and its block runs. - The program carries on after the
try/catchas if nothing had happened. The loop moves on to"6"normally.
e is the exception object. The most useful thing it has is e.getMessage(), the message (here, For input string: "7,5").
Putting the try outside the loop. Then, when "7,5" fails, the rest of the try is skipped… which is the entire loop, and "6" never gets processed. If you want every element to get its chance, the try goes inside the loop.
Several catch blocks
One try can have several catch blocks, one per kind of problem:
String[] data = {"8", "abc"};
int[] positions = {0, 1, 5};
for (int p : positions) {
try {
double n = Double.parseDouble(data[p]);
System.out.println("Position " + p + ": " + n);
} catch (NumberFormatException e) {
System.out.println("Position " + p + ": not a number");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Position " + p + ": " + e.getMessage());
}
}
Position 0: 8.0 Position 1: not a number Position 5: Index 5 out of bounds for length 2
Java tries the catch blocks from top to bottom and goes into the first one that matches. If none matches, the exception carries on as if there were no try at all.
Putting a catch (Exception e) first and a more specific one underneath. Exception is the mother of almost all of them, so it catches everything and the one below could never run. Java doesn’t allow it: exception NumberFormatException has already been caught. Rule: from the most specific to the most general.
If two exceptions are handled the same way, you can join them with | (multi-catch): catch (NumberFormatException | ArrayIndexOutOfBoundsException e).
finally: what always runs
A finally block always runs when leaving the try: whether there’s an exception or not, and even if there’s a return inside.
static double read(String text) {
try {
System.out.println("1. Trying to convert \"" + text + "\"");
double n = Double.parseDouble(text);
System.out.println("2. Converted: " + n);
return n;
} catch (NumberFormatException e) {
System.out.println("3. Not a number");
return -1;
} finally {
System.out.println("4. finally: this always runs");
}
}
System.out.println(read("9"));
System.out.println(read("nine"));
1. Trying to convert "9" 2. Converted: 9.0 4. finally: this always runs 9.0 1. Trying to convert "nine" 3. Not a number 4. finally: this always runs -1.0
Look at the order: with "9" it doesn’t go through 3; with "nine" it doesn’t go through 2. But 4 comes out both times, before the outer println receives the returned value: the return waits for the finally to finish.
What’s it for? For things that have to happen no matter what: closing a file, releasing a connection, writing a message to a log.
The exception family
Exceptions are classes, and they form an inheritance hierarchy, like Person → Student:
Throwable
├── Error (serious problems in the machine: not handled)
└── Exception
├── IOException (checked: files, network…)
│ └── FileNotFoundException
├── (your own exceptions, if they extend Exception)
└── RuntimeException (unchecked: programming errors)
├── NullPointerException
├── ArithmeticException
├── ClassCastException
├── IllegalArgumentException
│ └── NumberFormatException
└── IndexOutOfBoundsException
└── ArrayIndexOutOfBoundsException
Since it’s inheritance, a catch (IllegalArgumentException e) also catches a NumberFormatException: it is an IllegalArgumentException. It’s the same instanceof you saw in inheritance.
| Exception | When it’s thrown | Example |
|---|---|---|
NumberFormatException | Converting text that isn’t a number | Integer.parseInt("8a") |
ArrayIndexOutOfBoundsException | Index outside the array | grades[5] with 3 elements |
NullPointerException | Using something that is null | name.length() with name = null |
ArithmeticException | Integer division by 0 | 10 / 0 |
ClassCastException | Casting to a type it isn’t | (Student) teacher |
IllegalArgumentException | An invalid argument (you throw it) | setGrade(12) |
FileNotFoundException | The file doesn’t exist | new FileReader("x.txt") |
10.0 / 0 does not throw anything: with double it gives Infinity. ArithmeticException only shows up with integers.
Checked and unchecked
This distinction is the most confusing one, and it’s actually simple:
- Unchecked (
RuntimeExceptionand its children): usually programming errors that you avoid by checking first (a badly calculated index, anull). Java doesn’t force you to handle them. - Checked (the rest of
Exception, likeIOException): external problems that can happen even if your code is perfect (the file doesn’t exist, the network goes down). Java forces you to decide what to do with them.
If you call something that throws a checked exception and do nothing, it won’t even compile:
FileReader r = new FileReader("grades.txt");
error: unreported exception FileNotFoundException; must be caught or declared to be thrown
The message gives you both options:
- Catch it (caught): wrap it in
try/catch. - Declare it (declared to be thrown): add
throwsto your method so whoever calls you handles it.
static void open() throws FileNotFoundException { // "I don't handle it: let my caller handle it"
FileReader r = new FileReader("grades.txt");
}
throws (with an s, in the method signature) warns that something might come out. throw (no s, inside the code) throws the exception. They’re two different things that get mixed up a lot; throw is next.
throw: throwing your own exceptions
Until now, Java threw the exceptions. But you can throw them too when you get data that makes no sense:
static void checkGrade(double grade) {
if (grade < 0 || grade > 10) {
throw new IllegalArgumentException("Grade out of range: " + grade);
}
}
try {
checkGrade(7);
System.out.println("7 is valid");
checkGrade(12);
System.out.println("This line never runs");
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
7 is valid Error: Grade out of range: 12.0
throw new … creates the exception object (with its message) and throws it. From that moment the method stops, just like a return, but instead of returning a value it “sends a warning” upwards.
Why is this better than the println("Invalid grade") we used in classes and objects? Because a println is only seen by a person at the console; an exception can be detected and handled by the code that called the method, which decides what to do (ask for another grade, skip that student, show a message in the interface…).
Your own exceptions
IllegalArgumentException works for almost everything, but sometimes it’s worth having an exception with its own name, one that says exactly what happened and carries useful data. You create it with inheritance:
public class InvalidGradeException extends Exception {
private final double grade; // we keep the value that failed
public InvalidGradeException(double grade) {
super("Grade out of range: " + grade + " (must be between 0 and 10)");
this.grade = grade;
}
public double getGrade() { return grade; }
}
- It extends
Exception, so it’s checked: anyone who usessetGradewill have to handle it. (If it extendedRuntimeException, it would be unchecked.) super(message)passes the text toException‘s constructor, sogetMessage()works without you writing it.- It can have its own fields and methods, like
getGrade().
Now Student throws it:
public class Student {
private final String name;
private double grade;
public Student(String name) {
this.name = name;
}
public void setGrade(double grade) throws InvalidGradeException {
if (grade < 0 || grade > 10) {
throw new InvalidGradeException(grade); // it stops here: the grade doesn't change
}
this.grade = grade;
}
public String getName() { return name; }
public double getGrade() { return grade; }
}
And whoever uses it decides what to do with each problem:
public class Enrolment {
// It doesn't handle it: it lets it go up with throws
static void setGradeFromText(Student s, String text) throws InvalidGradeException {
double grade = Double.parseDouble(text); // may throw NumberFormatException
s.setGrade(grade); // may throw InvalidGradeException
}
public static void main(String[] args) {
Student ana = new Student("Ana");
String[] attempts = {"12", "eight", "8.5"};
for (String t : attempts) {
try {
setGradeFromText(ana, t);
System.out.println("Grade saved: " + ana.getGrade());
} catch (InvalidGradeException e) {
System.out.println(e.getMessage() + " -> you gave me " + e.getGrade());
} catch (NumberFormatException e) {
System.out.println("\"" + t + "\" is not a number");
}
}
}
}
Grade out of range: 12.0 (must be between 0 and 10) -> you gave me 12.0 "eight" is not a number Grade saved: 8.5
Propagating: the exception climbs up the stack
setGradeFromText has no try: it declares throws InvalidGradeException and lets it climb up to main, which is the one that handles it. This is called propagating. If nobody handled it, it would reach the very top and we’d see its stack trace. For example, with a main that also declares it with throws:
Exception in thread "main" InvalidGradeException: Grade out of range: 12.0 (must be between 0 and 10) at Student.setGrade(Student.java:12) at Unhandled.setGrade(Unhandled.java:4) at Unhandled.main(Unhandled.java:9)
Now the trace is all your code: it was thrown on line 12 of Student (the throw), passing through setGrade and main.
Handling every exception with an empty catch (Exception e) {} “so it doesn’t get in the way”. The program no longer stops, but you also never find out that something went wrong, and the bug shows up much later somewhere else. If you catch, do something: report it, fix it or throw it again.
try-with-resources: closing files without forgetting
An open file has to be closed, error or no error. It used to be done in a finally; since Java 7 there’s a more convenient way: declare the resource in brackets after try, and Java closes it on its own when it leaves:
try (BufferedReader reader = new BufferedReader(new FileReader("grades.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("Couldn't read: " + e.getMessage());
}
If grades.txt doesn’t exist, the output looks like this (the exact text depends on the operating system; on Windows it says The system cannot find the file specified):
Couldn't read: grades.txt (No such file or directory)
FileNotFoundException is a child of IOException, which is why catch (IOException e) catches it. You’ll use this properly in the practice article.
Watch it step by step in Python Tutor
Python Tutor runs code line by line and draws the variables and the call stack. Choose Java in the drop-down, paste this program and step through it with Next:
public class Tutor {
static double convert(String t) {
return Double.parseDouble(t); // this is where it can fail
}
static double average(String[] grades) {
double sum = 0;
for (String t : grades) {
sum += convert(t);
}
return sum / grades.length;
}
public static void main(String[] args) {
String[] grades = {"8", "6", "7,5"};
try {
System.out.println(average(grades));
} catch (NumberFormatException e) {
System.out.println("Error: " + e.getMessage());
} finally {
System.out.println("Done");
}
}
}
Output:
Error: For input string: "7,5" Done
What to watch:
- With
"8"and"6"you’ll see the stack grow and shrink:main→average→convertand back.sumreaches 14. - On the third pass, the call to
convert("7,5")doesn’t return anything: the exception is thrown andconvert‘s frame vanishes all at once. - Right after,
average‘s frame vanishes too, without reaching thereturn. That’s propagation: the exception “climbs” because nobody catches it there. - Execution reappears in
main‘scatch. The variablesumwas lost withaverage‘s frame: that’s why no average is ever printed. - Finally it goes through the
finallyand prints “Done”.
Summary and quick table
| Concept | In Java | If you come from Python |
|---|---|---|
| Try something that might fail | try { … } | try: |
| Catch a specific type | catch (NumberFormatException e) | except ValueError as e: |
| What always runs | finally { … } | finally: |
| Throw an exception | throw new IllegalArgumentException("…") | raise ValueError("…") |
| Warn that a method may throw it | throws InvalidGradeException | doesn’t exist |
| Your own exception | class X extends Exception | class X(Exception): |
| Close resources automatically | try (BufferedReader r = …) | with open(…) as f: |
| Forced to handle it | Yes, if it’s checked | Never |
Three ideas to take with you:
- Read the trace from the top: first line (what), first line of your code (where), the rest (how you got there).
- **When an exception is thrown, the rest of the
tryis skipped**; Java looks for the firstcatchthat matches and then carries on.finallyalways runs. - Checked forces you, unchecked doesn’t: checked exceptions are caught or declared with
throws; withthrowyou throw your own, and your own are created by extendingException.
In the practice article you’ll build three programs that don’t fall over with weird data: a grades importer, an ATM with its own exceptions and a file reader.
Next step: practiceExceptions in Java — practice with a grades importer, an ATM and a file reader (and the “Error” that told me nothing)Did this article help you? Here is how you can support SergioLearns →

2 Comments