The first time one of my Java programs crashed, I got a wall of red text that ended with something like Exception in thread "main". I had no idea what it meant, which line to look at or how to stop it from happening again.
Later I learned try and catch, and I thought I understood. Then more questions showed up. What's the difference between throw and throws? What does "ducking an exception" mean? Why does my Scanner crash when the user types a word instead of a number? And why do people say finally isn't enough anymore?
In this post, we'll answer all of that in one place: exceptions, the exception hierarchy, try, catch, finally, throw, throws, ducking, the Scanner class and try-with-resources. I've drawn the key pictures in text so you can see how things flow.
Unless I show a full class, the snippets go inside a main method.
What is an exception?
An exception is an unexpected event that happens while a program is running and disrupts its normal flow. Dividing by zero, reading a file that doesn't exist or calling a method on null are all examples.
Let's see what happens when nobody handles one:
public class Demo {
public static void main(String[] args) {
int a = 10, b = 0;
System.out.println("Before");
System.out.println(a / b);
System.out.println("After");
}
}
Output:
Before
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Demo.main(Demo.java:5)
Three things to notice:
-
"Before"printed, but"After"never did. The program stopped at the problem. - The first line tells you the type (
ArithmeticException) and a message (/ by zero). - The
at Demo.main(Demo.java:5)line is the stack trace. It tells you where it happened: fileDemo.java, line 5. When you read a crash, start at the top and look for the first line that points to your own code.
Behind the scenes: what the JVM does
When something goes wrong, the JVM creates an exception object, fills it with the details (type, message and the call stack) and throws it. The current method stops immediately. Then the JVM goes looking for code that can handle it, first in the current method, then in the method that called it, then the one that called that, moving up the call stack.
main() calls process() calls divide() <-- exception happens here
^ ^ |
| | |
+---------------+-----------------+
the JVM unwinds UP the call stack, looking for a matching catch
Nobody catches it? The thread ends and the stack trace is printed.
Exception handling is simply the art of catching it on the way up so the program can continue.
The exception hierarchy
Exceptions are objects, so they're organized into a family tree of classes. Knowing this tree tells you which exceptions you must handle and which you can ignore.
Throwable
/ \
Error Exception
(OutOfMemoryError, / \
StackOverflowError) IOException RuntimeException
SQLException ArithmeticException
FileNotFound- NullPointerException
Exception ArrayIndexOutOfBounds-
InterruptedException Exception
IllegalArgumentException
(CHECKED) NumberFormatException
InputMismatchException
(UNCHECKED)
Here's what each branch means:
-
Throwableis the root. Only objects of this class (or its children) can be thrown. It gives yougetMessage(),printStackTrace()andgetCause(). -
Errormeans a serious problem in the JVM itself, like running out of memory (OutOfMemoryError) or endless recursion (StackOverflowError). You normally don't catch these, because your program can't repair them. -
Exceptionis for problems your program can reasonably handle. It splits into two kinds:
Checked exceptions are everything under Exception except RuntimeException. They're for things outside your program's control, like missing files or network failures (IOException, SQLException). The compiler forces you to handle them or declare them.
new FileReader("data.txt");
unreported exception FileNotFoundException; must be caught or declared to be thrown
Unchecked exceptions are RuntimeException and its children. They usually mean a bug in your code (a bad index, a null reference, a bad argument). The compiler does not force you to handle them, although you still can.
A simple way to remember it: checked means "the compiler checks that you dealt with it," and unchecked means "the compiler leaves it to you."
try and catch
To handle an exception, wrap the risky code in try and write the response in catch:
try {
int result = 10 / 0;
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero: " + e.getMessage());
}
System.out.println("Program continues");
Output:
Cannot divide by zero: / by zero
Program continues
What happened:
- Java ran the
tryblock until the exception happened. - It skipped the rest of the
tryblock (so"Result: ..."never printed). - It jumped to the matching
catchand ran it. - The program continued normally after the
try-catch. No crash.
The variable e is the exception object. Printing it directly gives the type and message together: java.lang.ArithmeticException: / by zero.
Multiple catch blocks
One try can have several catch blocks for different problems. Java runs only the first one that matches:
String[] data = {"10", "abc"};
try {
int n = Integer.parseInt(data[1]);
System.out.println(10 / n);
} catch (NumberFormatException e) {
System.out.println("Not a number: " + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("Math problem");
} catch (Exception e) {
System.out.println("Something else: " + e);
}
Output:
Not a number: For input string: "abc"
Order matters. Put the most specific exceptions first and the most general last. If you put Exception first, it would catch everything, and the later blocks could never run. The compiler notices and stops you:
exception ArithmeticException has already been caught
If two exceptions need the same handling, you can combine them with |:
catch (NumberFormatException | ArithmeticException e) {
System.out.println("Bad input: " + e.getMessage());
}
(The two types can't be parent and child of each other.)
Behind the scenes
The compiler doesn't turn try-catch into extra checks that run on every line. Instead, it writes an exception table next to your bytecode. Each row says: "if an exception of this type happens between instruction A and instruction B, jump to the handler at instruction C." When nothing goes wrong, the table is never used, so a try block costs almost nothing. The real cost comes when an exception is created, because the JVM has to record the stack trace.
finally
Sometimes you need code that runs no matter what, like closing a file or releasing a lock. That's what finally is for:
static void test(int divisor) {
try {
System.out.println(10 / divisor);
} catch (ArithmeticException e) {
System.out.println("Caught");
} finally {
System.out.println("Cleanup");
}
}
test(2);
test(0);
Output:
5
Cleanup
Caught
Cleanup
The finally block ran in both cases: when everything went fine, and when an exception happened. Here is the flow:
try block
|
exception happened?
/ \
no yes
| |
| matching catch? -- yes --> run the catch block
| | |
| no |
| (exception will keep going up) |
v v v
+--------------------------------------------------+
| finally block ALWAYS runs here |
+--------------------------------------------------+
|
v
continue the program, or keep propagating the exception
try-catch-finally, try-finally
The combinations you can write:
-
try+catch(one or more) -
try+catch+finally -
try+finally(no catch at all)
The last one is valid, and it's useful when you want to clean up but let the exception continue to the caller:
try {
System.out.println("working");
throw new RuntimeException("boom");
} finally {
System.out.println("cleanup");
}
Output:
working
cleanup
Exception in thread "main" java.lang.RuntimeException: boom
The cleanup ran first, and then the exception continued upward and crashed the program, because nobody caught it.
When does finally NOT run?
Almost never. It runs even if the try block has a return:
static int f() {
try {
return 1;
} finally {
System.out.println("finally runs");
}
}
System.out.println(f());
Output:
finally runs
1
It will not run if the JVM stops (for example System.exit(0)), if the machine crashes or if the try block never ends (an infinite loop).
One warning: never write return or throw inside finally. It can silently replace the real result or hide an exception.
Behind the scenes
The compiler copies the finally code to every exit path: the normal end of try, the end of each catch, and every return. It also adds an "any exception" handler that runs the finally code and then rethrows. That's how Java promises it always runs.
throw vs throws
These two look almost the same and trip up everyone, but they do different jobs.
throw is a statement that actually throws an exception object. You use it inside a method body when you detect a problem.
static void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
System.out.println("Age set to " + age);
}
try {
setAge(25);
setAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
Output:
Age set to 25
Error: Age cannot be negative
throws is part of a method's signature. It warns callers that this method might throw certain exceptions, so they must be ready.
void readFile(String path) throws IOException, SQLException { ... }
Here's the difference in short:
-
throwthrows one exception object, and it's written inside the method body, followed bynew SomeException(...). -
throwsdeclares possible exceptions, and it's written after the parameter list, followed by class names (one or more, separated by commas). -
throwis the action.throwsis the warning label. -
throwsis required for checked exceptions (otherwise the compiler complains). For unchecked ones it's optional.
A trick to remember it: throw, singular, is one throw. throws, plural, can list many.
Custom exceptions
You can write your own exception by extending Exception (checked) or RuntimeException (unchecked). Let's make a checked one for a bank account:
class InsufficientFundsException extends Exception {
InsufficientFundsException(String message) {
super(message);
}
}
class Account {
private double balance = 500;
void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(
"Balance " + balance + " is less than " + amount);
}
balance -= amount;
System.out.println("Withdrawn " + amount);
}
}
Account account = new Account();
try {
account.withdraw(200);
account.withdraw(600);
} catch (InsufficientFundsException e) {
System.out.println("Failed: " + e.getMessage());
}
Output:
Withdrawn 200.0
Failed: Balance 300.0 is less than 600.0
Because InsufficientFundsException is checked, the compiler forced two things: withdraw had to declare throws, and the caller had to catch it (or declare it too).
Ducking an exception
Now the funny word. Ducking an exception means not handling it in this method, and passing it on to the caller by declaring it with throws. The method "ducks" out of the way, and the problem flies over its head to whoever called it.
static void level3() throws Exception {
throw new Exception("Problem in level3");
}
static void level2() throws Exception {
level3(); // no try-catch here: level2 ducks it
}
static void level1() {
try {
level2();
} catch (Exception e) {
System.out.println("Handled in level1: " + e.getMessage());
}
}
level1();
Output:
Handled in level1: Problem in level3
level1() --calls--> level2() --calls--> level3()
^ ^ |
| | | throw new Exception(...)
| +----- DUCKS ---------+ (just declares "throws")
|
+------- CATCHES it and handles the problem
When should you catch, and when should you duck?
- Catch where you can do something useful: show a message, retry, use a default value or clean up.
- Duck when this method can't decide what to do, and a higher-level method is in a better position. A low-level file reader doesn't know whether to show a popup or write a log, but the code that called it does.
Be careful with one case: if main ducks (public static void main(String[] args) throws Exception), the exception goes to the JVM, and your program crashes. That's fine for quick experiments, but not for real programs.
The Scanner class
Now let's apply this to something you'll use constantly. The Scanner class (in java.util) reads input: from the keyboard, from a file or from a string. It splits the input into pieces called tokens, separated by whitespace by default.
The most-used methods are:
-
nextInt(),nextDouble(),nextBoolean()read the next token as that type. -
next()reads the next word (one token). -
nextLine()reads the whole rest of the line. -
hasNextInt(),hasNext()check whether the next token exists or has the right type, without reading it. -
close()releases the resource.
import java.util.Scanner;
public class Greeting {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter your name: ");
String name = sc.nextLine();
System.out.print("Enter your age: ");
int age = sc.nextInt();
System.out.println("Hello " + name + ", next year you will be " + (age + 1));
sc.close();
}
}
A sample run (the second part of each line is what the user types):
Enter your name: Asha
Enter your age: 25
Hello Asha, next year you will be 26
Behind the scenes
System.in is a stream of raw bytes from the keyboard. The Scanner wraps it, reads the bytes into a buffer, and hands them to you one token at a time:
keyboard --> System.in (bytes) --> Scanner's buffer --> tokens --> your variables
You type: 25 [Enter]
Buffer: 2 5 \n
^^^^
nextInt() takes the token "25" and checks it looks like an int
The "\n" (the Enter key) stays behind in the buffer
Two details here matter for everything that follows:
-
nextInt()takes the token, but it leaves the newline (\n) in the buffer. - If the token isn't a valid number,
nextInt()throws anInputMismatchExceptionand leaves the bad token in the buffer (it does not consume it).
The famous nextLine trap
Let's flip the order of our program and read the age first, then the name:
System.out.print("Enter age: ");
int age = sc.nextInt();
System.out.print("Enter name: ");
String name = sc.nextLine();
System.out.println("Age: " + age + ", Name: [" + name + "]");
A sample run:
Enter age: 25
Enter name: Age: 25, Name: []
The program never waited for the name! After nextInt(), the buffer still held \n. Then nextLine() read from the current position to the end of the line, which is nothing, so it returned an empty string right away.
Buffer after nextInt(): \n
^^
nextLine() reads "everything up to the newline", which is an empty text
The fix is to eat the leftover newline with an extra nextLine():
int age = sc.nextInt();
sc.nextLine(); // consume the leftover newline
String name = sc.nextLine(); // now this waits for the real name
Another safe option is to read everything with nextLine() and convert yourself: int age = Integer.parseInt(sc.nextLine());.
Exceptions that Scanner can throw
-
InputMismatchException(unchecked): the token isn't the right type, like "abc" fornextInt(). -
NoSuchElementException(unchecked): there's no more input to read. -
IllegalStateException(unchecked): you used the scanner after closing it. -
FileNotFoundException(checked): when creating aScannerfrom aFilethat doesn't exist.
Scanner with exception handling
Here's the classic job: keep asking until the user types a valid age.
import java.util.InputMismatchException;
import java.util.Scanner;
public class SafeInput {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int age = 0;
boolean valid = false;
while (!valid) {
try {
System.out.print("Enter your age: ");
age = sc.nextInt();
if (age < 0 || age > 120) {
throw new IllegalArgumentException("Age must be between 0 and 120");
}
valid = true;
} catch (InputMismatchException e) {
System.out.println("Please enter digits only.");
sc.nextLine(); // throw away the bad input
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
System.out.println("Age accepted: " + age);
}
}
A sample run, where the user types abc, then -5, then 25:
Enter your age: abc
Please enter digits only.
Enter your age: -5
Age must be between 0 and 120
Enter your age: 25
Age accepted: 25
Look at how our new tools work together:
-
nextInt()throwsInputMismatchExceptionforabc, and thecatchhandles it. - We used
throwourselves for a value that is a valid number but makes no sense (-5). - The
sc.nextLine()inside the firstcatchis essential. Remember: the bad token stays in the buffer. Without that line, the nextnextInt()would seeabcagain, throw again, and the loop would repeat forever.
An alternative without exceptions
When bad input is expected (users make typos all the time), you can check first instead of catching:
Scanner sc = new Scanner(System.in);
System.out.print("Enter a number: ");
while (!sc.hasNextInt()) {
System.out.println("Digits only, please.");
sc.next(); // discard the bad token
System.out.print("Enter a number: ");
}
int n = sc.nextInt();
System.out.println("You entered " + n);
A sample run:
Enter a number: hello
Digits only, please.
Enter a number: 7
You entered 7
Both approaches are fine. The hasNextInt() style treats bad input as normal, so it needs no exception at all.
try-with-resources
Many things you use must be closed when you're done: a Scanner, files, network connections, database connections. If you forget, they leak. The old way was a finally block:
Scanner sc = null;
try {
sc = new Scanner(new File("data.txt"));
System.out.println(sc.nextLine());
} catch (FileNotFoundException e) {
System.out.println("File not found");
} finally {
if (sc != null) {
sc.close();
}
}
That's a lot of code just to close something, and it's easy to get wrong. try-with-resources does it for you. You open the resource inside parentheses after try, and Java closes it automatically at the end:
try (Scanner sc = new Scanner(new File("data.txt"))) {
System.out.println(sc.nextLine());
} catch (FileNotFoundException e) {
System.out.println("File not found");
}
No finally, no null check, no close() call. The rules:
- The resource's class must implement
AutoCloseable(orCloseable, which extends it).Scannerdoes. - The resource variable is implicitly
final. - You can open several resources, separated by semicolons.
-
catchandfinallyblocks are optional. - Since Java 9, you can use an existing effectively final variable:
try (sc) { ... }.
What happens in what order?
Let's use a custom resource to see the exact order:
class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
System.out.println("Open " + name);
}
void use() {
System.out.println("Using " + name);
}
@Override
public void close() {
System.out.println("Close " + name);
}
}
try (Resource a = new Resource("A"); Resource b = new Resource("B")) {
a.use();
b.use();
throw new RuntimeException("Oops");
} catch (RuntimeException e) {
System.out.println("Caught " + e.getMessage());
} finally {
System.out.println("Finally");
}
Output:
Open A
Open B
Using A
Using B
Close B
Close A
Caught Oops
Finally
Two rules are visible here:
- Resources are closed in reverse order of opening (B before A), like unstacking plates.
- They're closed before the
catchandfinallyblocks run. So by the time yourcatchexecutes, the resources are already safely closed.
Behind the scenes
The compiler rewrites try-with-resources into a try-finally, roughly like this:
try (Resource r = new Resource()) { Resource r = new Resource();
use(r); ==> Throwable primary = null;
} try {
use(r);
} catch (Throwable t) {
primary = t; throw t;
} finally {
if (r != null) {
if (primary != null) {
try { r.close(); }
catch (Throwable s) { primary.addSuppressed(s); }
} else {
r.close();
}
}
}
You don't need to memorize this. The key part is the suppressed exception handling. What if your code throws an exception and close() also throws? With a hand-written finally, the second exception would replace the first one, and you'd lose the real cause. With try-with-resources, the original exception stays as the main one, and the close failure is attached to it as a suppressed exception that you can read with e.getSuppressed(). That's one more reason it's better than a manual finally.
Here's the earlier idea with a Scanner, fully contained, with no files and no keyboard, so you can run it right now:
try (Scanner sc = new Scanner("10 20 abc")) {
int sum = 0;
while (sc.hasNext()) {
sum += sc.nextInt();
}
System.out.println(sum);
} catch (InputMismatchException e) {
System.out.println("Bad number found");
}
Output:
Bad number found
A Scanner can read from a plain String, so "10 20 abc" is our fake input. It added 10 and 20, then hit abc and threw. The scanner was closed automatically, and the catch handled the problem.
One note about the keyboard: if you wrap new Scanner(System.in) in try-with-resources, closing it also closes System.in, and you can't read from the keyboard again later in the same program. For a console program that finishes right after, that's fine. Just don't create and close several scanners on System.in.
Which one should I use?
- try-catch or just let it crash? Catch an exception when you can do something useful about it (retry, use a default, show a clear message). If you can't, let it travel up, or duck it.
- Catch or duck? Catch it in the method that knows what to do. Duck it when a higher level is in a better position to decide.
-
try-catch-finally or try-with-resources? If the thing you're closing implements
AutoCloseable(files, scanners, connections), use try-with-resources. Use a plainfinallyonly for cleanup that isn't a closeable resource, like resetting a flag or unlocking a lock. -
Checked or unchecked for my own exceptions? Make it checked when the caller can realistically recover from it (like
InsufficientFundsException). Make it unchecked for programming mistakes (like a bad argument). Modern code leans toward unchecked, because checked ones force a lot of boilerplate. -
Scanner:
hasNextInt()or try-catch? UsehasNextInt()when bad input is a normal, expected thing. Use try-catch when you read many values and want a single place to handle failure.
Common mistakes
-
Empty catch blocks.
catch (Exception e) { }hides the problem completely. At the very least, print or log the exception. -
Catching
ExceptionorThrowableeverywhere. It also swallows bugs you wanted to see, andThrowableeven catches seriousErrors. Catch the most specific type you can handle. - Putting a general catch before a specific one. The compiler says the specific exception "has already been caught." Specific first, general last.
- Forgetting to close resources. Use try-with-resources so you never have to remember.
-
Falling into the Scanner traps. Using
nextLine()right afternextInt()without consuming the newline, and not clearing a bad token afterInputMismatchException(which causes an infinite loop). -
Mixing up
throwandthrows.throwgoes in the body with an object (throw new X()).throwsgoes on the method signature with class names. Writingthrows new X()orthrow Xis a compile error.
Another habit to avoid is using exceptions for normal flow, like looping until an exception happens. Exceptions are for exceptional situations, and they're slower than a simple if.
Quick recap
- An exception is an object that represents a problem at run time. If nobody handles it, the program stops with a stack trace.
- The tree:
Throwablesplits intoError(don't catch) andException. UnderException, checked exceptions must be handled or declared, and unchecked ones (RuntimeExceptionand children) are optional. -
trywraps risky code,catchhandles it (specific first, general last), andfinallyalways runs. -
throwthrows one exception object inside a method.throwsdeclares what a method might throw. -
Ducking means declaring
throwsinstead of catching, so the caller deals with it. -
Scannerreads tokens from the keyboard, files or strings.nextInt()leaves the newline behind, and a bad token stays in the buffer. - Handle bad input with
try-catchplussc.nextLine()to clear it, or check ahead withhasNextInt(). -
try-with-resources closes
AutoCloseableresources automatically, in reverse order, beforecatchandfinally, and it keeps the original exception safe.
Try it yourself
Here's a small practice idea: build a console calculator. Ask the user for two numbers and an operator (+, -, *, /) using Scanner. Keep asking until the numbers are valid. Handle division by zero with try-catch, and throw your own InvalidOperatorException (a custom checked exception) if the operator isn't one of the four. Wrap the whole Scanner in try-with-resources.
For a bonus experiment, create a text file with a few numbers and read it using new Scanner(new File("numbers.txt")). Handle the FileNotFoundException, then try renaming the file to see the exception in action.
If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.
Top comments (0)