10 Lesson 5 of 8

Exception Propagation

How exceptions travel up the call stack

Call Stack Propagation throws Handler

1 What is Propagation?

When a method encounters a problem it can't fix, it throws an exception. But what happens to that exception? Where does it go?

The exception travels UP the call stack — from the method that threw it, to the method that called it, to the method that called THAT method, and so on — until someone catches it.

If no one catches it, the exception reaches main(). If main() doesn't catch it either, the program crashes.

The Two Decisions
We are always about two things — stopping the program gracefully and continuing the program. The main method (or whichever method catches the exception) takes this decision.

2 The Municipality Analogy

Imagine a father asks his son (the student) to go to Jeddah municipality (أمانة جدة) to follow up on an application for electricity. The father has been waiting for this application for one year. He gives the student the papers and sends him.

The student arrives at the counter. The employee checks the application and finds that a document is missing — the father's national ID. Can the employee fix this? Absolutely not. This is a document that only the father has. All the employee can do is REPORT the problem back to the student: "Sorry, the national ID is missing. I cannot process this."

Now the student has four possible reactions:

Situation 1: Ignore the Problem (Catastrophic!)

The student goes to a coffee shop, forgets about the documents, and never returns to his father. The father is still waiting at home, wondering what happened. The application is stuck forever.

In code: this is like catching an exception and doing nothing — swallowing the error silently. The program continues but the problem is never resolved. The caller never knows what went wrong.

Situation 2: Fix It Himself (Simple Problem)

The employee tells him: "This document needs two copies, but you only have one." Easy! The student walks to a nearby stationery shop, makes a copy, comes back, and the application is processed.

In code: this is like catching the exception and handling it locally — the caller knows how to fix the problem and recovers on its own. For example, asking the user for new input.

Situation 3: Cancel the Application (Destructive!)

The student says: "Forget it, just cancel the whole application." But his father has been waiting for one year! The student just destroyed everything in one moment without consulting his father.

In code: this is like calling System.exit(1) inside the method — terminating the entire program without giving the caller a chance to decide. Very dangerous.

Situation 4: Go Back to His Father (The Right Way!)

The student goes back to his father and says: "They said a document is missing." Now the father — the one who started this whole process — can make the right decision: provide the missing document, try a different approach, or cancel the application knowingly.

In code: this is exception propagation — the exception travels up the call chain until it reaches someone who can properly handle it. This is the correct approach.

The Mapping to Code

Imagine this call chain: main() calls method1(), and method1() calls method2().

The father = main() — the one who started the whole process
The student = method1() — sent by main to do the job
The employee = method2() — does the actual work, but finds a problem
"National ID is missing" = the exception object

method2() reports the problem to method1(). If method1() cannot handle it, the exception propagates up to main() — the one who can make the right decision.

Why Must the Caller Decide?

You might ask: why does it matter who handles the error? Why can't the method just close the program or ignore the problem? Here are two real-world examples that show why the caller must have the right to decide whether to keep the program running or to stop it:

Example 1: Bank Transaction

Consider a bank transfer: move money from Ahmed's account to Omar's account. The system deducts from Ahmed's account but crashes before depositing to Omar's account.

Where did the money go? If we don't handle this properly, Ahmed loses money and Omar gets nothing!

The caller (the main banking flow) needs to know what happened so it can ROLLBACK the transaction — put the money back into Ahmed's account. If the method had terminated the program on its own, there would be no chance to rollback. The caller must stay in control.

Example 2: File Handling

When you open a file in Java, the operating system marks that file as "in use" by your program.

If your program crashes while the file is open, the OS still thinks the file is in use. Have you ever tried to delete a file and Windows says "Cannot delete — file in use by another process"? That happens because a program crashed without properly closing the file.

The caller needs the chance to close the file properly before anything else happens. If the method had terminated the program, the file stays locked. Exception handling (specifically try-finally and try-with-resources) guarantees that cleanup always happens, even when things go wrong.

3 Visual Example — The Call Stack

Let's trace an exception through a chain of method calls.

main() — the decision maker
↑ exception propagates
method1() — can't handle it
↑ exception propagates
method2() — can't handle it
↑ exception propagates
method3() — throws exception!
How It Works

method3() throws the exception. method2() doesn't know how to fix it, so the exception propagates to method1(). method1() doesn't know either, so it propagates to main(). main() decides: handle it and continue, or terminate gracefully.

4 Complete Code Example

ExceptionPropagationDemo.java
public class ExceptionPropagationDemo { public static void main(String[] args) { try { m1(); System.out.println("S1"); } catch (ArithmeticException e) { System.out.println(e.getMessage()); } System.out.println("S2"); } public static void m1() { try { m2(); System.out.println("S3"); } catch (NullPointerException e) { System.out.println(e.getMessage()); } System.out.println("S4"); } public static void m2() { try { m3(); System.out.println("S5"); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } System.out.println("S6"); } public static void m3() { throw new IllegalArgumentException("Illegal wrong"); } }
Console Output
Illegal wrong S6 S3 S4 S1 S2

5 Step-by-Step Trace

main() — catches ArithmeticException
m1() — catches NullPointerException
m2() — catches IllegalArgumentException ✓
m3() — throws IllegalArgumentException
Trace Through the Code
  1. m3() throws IllegalArgumentException
  2. m2() catches it (IllegalArgumentException matches). Prints "Illegal wrong", then "S6"
  3. Control returns to m1() — the call to m2() completed normally (exception was handled). Prints "S3", then "S4"
  4. Control returns to main() — the call to m1() completed normally. Prints "S1", then "S2"

6 What If No One Catches It?

Let's change the example. What if m2() catches a DIFFERENT exception type?

Modified m2() — wrong catch type
public static void m2() { try { m3(); System.out.println("S5"); } catch (ArrayIndexOutOfBoundsException e) { // Wrong type! System.out.println(e.getMessage()); } System.out.println("S6"); } public static void m3() { throw new IllegalArgumentException("Illegal wrong"); }

Now m2() can't catch IllegalArgumentException — it only catches ArrayIndexOutOfBoundsException. So the exception propagates to m1().

m1() catches NullPointerException — wrong type again! The exception propagates to main().

main() catches ArithmeticException — wrong type again!

No One Caught the Exception!
No method in the entire call chain had a matching catch block for IllegalArgumentException. The exception reached the top of the call stack with no handler — the program crashes with an unhandled exception error.
Console Output — Crash!
Exception in thread "main" java.lang.IllegalArgumentException: Illegal wrong at ExceptionPropagationDemo.m3(ExceptionPropagationDemo.java:27) at ExceptionPropagationDemo.m2(ExceptionPropagationDemo.java:19) at ExceptionPropagationDemo.m1(ExceptionPropagationDemo.java:11) at ExceptionPropagationDemo.main(ExceptionPropagationDemo.java:4)

7 Why Didn’t It Match? — Revisiting the Hierarchy

Let’s stop and think. In the previous example, m3() threw IllegalArgumentException, and m2() tried to catch ArrayIndexOutOfBoundsException. It didn’t work. Why?

Because they are siblings — they are both children of RuntimeException, but they are not related to each other by inheritance.

The Hierarchy

RuntimeException

└— IllegalArgumentException

└— IndexOutOfBoundsException

└— ArrayIndexOutOfBoundsException

ArrayIndexOutOfBoundsException is NOT a parent of IllegalArgumentException. They are in different branches. So the catch does not match.

Question What if we catch with a supertype?

What if m2() catches RuntimeException instead?

Modified m2() — catching the supertype
public static void m2() { try { m3(); System.out.println("S5"); } catch (RuntimeException e) { // Supertype of IllegalArgumentException System.out.println(e.getMessage()); } System.out.println("S6"); } public static void m3() { throw new IllegalArgumentException("Illegal wrong"); }
It Works!

RuntimeException is a parent of IllegalArgumentException. A catch block catches the specified exception and all its subclasses. So catch (RuntimeException e) catches IllegalArgumentException, ArithmeticException, NullPointerException — any child of RuntimeException.

Console Output
Illegal wrong S6 S3 S4 S1 S2
The Rule

A catch block matches if the thrown exception is the same type or a subclass of the declared type. This is polymorphism — a concept you already know from Chapter 11.

catch (RuntimeException e) → catches any RuntimeException subclass
catch (Exception e) → catches any Exception subclass
catch (IllegalArgumentException e) → catches only IllegalArgumentException and its subclasses

Now You See Why the Hierarchy Matters

When we studied the exception hierarchy in Lesson 4, you might have thought: why do I need to know all these classes? Now you see — the hierarchy tells you which catch block will match. If you catch a parent type, you catch all the children. If you catch a sibling type, you catch nothing.

8 Summary

Key Takeaways
  • Exceptions travel UP the call stack from thrower to caller
  • Each method on the stack can catch or let it propagate
  • If no one catches it → program crashes
  • A catch block matches the same type or any subclass (polymorphism)
  • The method that catches decides: handle and continue, or terminate gracefully
Coming Up Next

In the next lesson, we will learn the complete syntax for handling exceptions: try, catch, finally, and try-with-resources.

← Lesson 4: Exception Hierarchy Lesson 6: try-catch-finally →
Code Block Theme