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()); } catchSource:https://dev.to/mayur_bodhare/throw-vs-throws-in-java-and-what-ducking-means-5f8hRelated posts
DeepSeek's Open-Source "Super-Agent" Codes Without Being Asked
All-in-one platform or a stack you assemble? Where do coaches and educators land?
I Wanted to Live on Coruscant. PAT Scared Me. Now My Alexa Lives on Mute.
Detect What You Can Name: PaliGemma 2 Object Detection with Transformers
When Should an LLM Router Stop Being Sticky? llm-d 1.0 Cache-Affinity Routing, Break-Even Math, and a Simulation
