diff --git a/README.md b/README.md
index fe9e7b7..9cb01fc 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ article; each module's own README has that article's version table, quickstart,
| [`concurrency-interview`](concurrency-interview/) | Top 40 Java Concurrency Interview Questions and Answers (2026) |
| [`arithmetic`](arithmetic/) | Add Two Numbers in Java Without Overflow: addExact, Widening, and BigInteger |
| [`hashmap-concurrenthashmap`](hashmap-concurrenthashmap/) | Java HashMap vs ConcurrentHashMap: Complete Interview Guide |
+| [`exceptions`](exceptions/) | Java Exception Handling Deep Dive: Checked vs Unchecked, Suppression, and What Exceptions Actually Cost |
## License
diff --git a/exceptions/README.md b/exceptions/README.md
new file mode 100644
index 0000000..027371e
--- /dev/null
+++ b/exceptions/README.md
@@ -0,0 +1,40 @@
+# exceptions
+
+Companion code for the ankurm.com post *"Java Exception Handling Deep Dive: Checked vs Unchecked,
+Suppression, and What Exceptions Actually Cost."* Module `exceptions` in `java-core-examples`.
+
+All explanation lives in the post; this module holds the runnable evidence and the captured output.
+
+## Versions
+
+| Component | Version |
+|---|---|
+| JDK | 25.0.4.1+1 (Temurin, LTS) |
+| JMH | 1.37 |
+| JUnit Jupiter | 5.11.0 |
+| Hardware | 2 vCPU x86-64 VM (timings are indicative, not a leaderboard) |
+
+## Quickstart
+
+```bash
+export JDK25_HOME=/path/to/jdk-25
+./scripts/run-all.sh # rebuilds and regenerates everything in output/
+```
+
+## What is in here
+
+| File | Shows | Output |
+|---|---|---|
+| `SuppressedDemo` | try/finally loses the original exception; try-with-resources suppresses the close() failure instead | `01` |
+| `ChainingDemo` | `Caused by` / `... N more`, and wrapping without the cause | `02` |
+| `CheckedVsUncheckedDemo` | which types are checked; `UncheckedIOException` in a lambda | `03` |
+| `CustomHierarchyDemo` | an unchecked base type with an error code; the 4-argument `Throwable` constructor | `04` |
+| `FinallyTrapDemo` | `return` inside `finally` discards the exception | `05` |
+| `src/broken/*.java` | four sources that deliberately fail to compile; javac's real message is captured | `06`-`09` |
+| `OmitStackTraceDemo` | HotSpot replaces a hot implicit exception with one that has no stack trace | `10` |
+| `ExceptionCostBenchmark` | JMH: normal vs no-stack-trace vs preallocated vs return code, at three stack depths | `11` |
+| `DeepTraceDemo` | stack traces are capped at `MaxJavaStackTraceDepth` = 1024 frames | `12`, `14` |
+| `ExceptionBehaviourTest` | 8 assertions behind the claims above | `13` |
+
+The JMH run is one fork, 3 warmup and 5 measurement iterations of 1 s; re-running moves the
+numbers by tens of percent but not the ordering.
diff --git a/exceptions/output/01-suppressed.txt b/exceptions/output/01-suppressed.txt
new file mode 100644
index 0000000..ac22215
--- /dev/null
+++ b/exceptions/output/01-suppressed.txt
@@ -0,0 +1,19 @@
+=== try/finally: the close() exception replaces the query() exception ===
+caught: java.lang.IllegalArgumentException: close failed on db-1
+suppressed count: 0
+
+=== try-with-resources: query() exception wins, close() exception is attached ===
+caught: java.lang.IllegalStateException: query failed on db-1
+suppressed count: 1
+ suppressed: java.lang.IllegalArgumentException: close failed on db-1
+
+=== two resources: closed in reverse order, each failure is suppressed ===
+caught: java.lang.IllegalStateException: query failed on B
+ suppressed: java.lang.IllegalArgumentException: close failed on B
+ suppressed: java.lang.IllegalArgumentException: close failed on A
+
+=== what printStackTrace() shows for suppressed exceptions ===
+java.lang.IllegalStateException: query failed on db-1
+ at com.ankurm.exceptions.SuppressedDemo$Conn.query(SuppressedDemo.java:9)
+ Suppressed: java.lang.IllegalArgumentException: close failed on db-1
+ at com.ankurm.exceptions.SuppressedDemo$Conn.close(SuppressedDemo.java:10)
diff --git a/exceptions/output/02-chaining.txt b/exceptions/output/02-chaining.txt
new file mode 100644
index 0000000..23bcc52
--- /dev/null
+++ b/exceptions/output/02-chaining.txt
@@ -0,0 +1,20 @@
+=== wrapped WITH the cause ===
+getMessage(): could not load order 42
+getCause(): java.lang.IllegalStateException: connection reset by peer
+java.lang.RuntimeException: could not load order 42
+ at com.ankurm.exceptions.ChainingDemo.serviceGood(ChainingDemo.java:10)
+ at com.ankurm.exceptions.ChainingDemo.print(ChainingDemo.java:20)
+ at com.ankurm.exceptions.ChainingDemo.main(ChainingDemo.java:30)
+Caused by: java.lang.IllegalStateException: connection reset by peer
+ at com.ankurm.exceptions.ChainingDemo.dao(ChainingDemo.java:6)
+ at com.ankurm.exceptions.ChainingDemo.serviceGood(ChainingDemo.java:9)
+ ... 2 more
+
+=== wrapped WITHOUT the cause (message copied as a string) ===
+getMessage(): could not load order 42: connection reset by peer
+getCause(): null
+java.lang.RuntimeException: could not load order 42: connection reset by peer
+ at com.ankurm.exceptions.ChainingDemo.serviceBad(ChainingDemo.java:15)
+ at com.ankurm.exceptions.ChainingDemo.print(ChainingDemo.java:20)
+ at com.ankurm.exceptions.ChainingDemo.main(ChainingDemo.java:31)
+
diff --git a/exceptions/output/03-checked-vs-unchecked.txt b/exceptions/output/03-checked-vs-unchecked.txt
new file mode 100644
index 0000000..edf9ba1
--- /dev/null
+++ b/exceptions/output/03-checked-vs-unchecked.txt
@@ -0,0 +1,15 @@
+=== which are checked? ===
+Exception CHECKED Exception -> Throwable -> Object
+IOException CHECKED IOException -> Exception -> Throwable -> Object
+InterruptedException CHECKED InterruptedException -> Exception -> Throwable -> Object
+RuntimeException unchecked RuntimeException -> Exception -> Throwable -> Object
+IllegalArgumentException unchecked IllegalArgumentException -> RuntimeException -> Exception -> Throwable -> Object
+NullPointerException unchecked NullPointerException -> RuntimeException -> Exception -> Throwable -> Object
+Error unchecked Error -> Throwable -> Object
+OutOfMemoryError unchecked OutOfMemoryError -> VirtualMachineError -> Error -> Throwable -> Object
+StackOverflowError unchecked StackOverflowError -> VirtualMachineError -> Error -> Throwable -> Object
+
+=== checked exception inside a stream lambda: wrap in UncheckedIOException ===
+contents of a.txt
+caught: java.io.UncheckedIOException: java.io.IOException: cannot read bad.txt
+original cause: java.io.IOException: cannot read bad.txt
diff --git a/exceptions/output/04-custom-hierarchy.txt b/exceptions/output/04-custom-hierarchy.txt
new file mode 100644
index 0000000..1e8d03c
--- /dev/null
+++ b/exceptions/output/04-custom-hierarchy.txt
@@ -0,0 +1,5 @@
+404-ish -> NOT_FOUND: order 42 does not exist
+generic -> CONFLICT: version mismatch
+ControlFlowException stack trace length: 0
+RuntimeException stack trace length: 1
+ControlFlowException suppressed count after addSuppressed(): 0
diff --git a/exceptions/output/05-finally-trap.txt b/exceptions/output/05-finally-trap.txt
new file mode 100644
index 0000000..5e76e83
--- /dev/null
+++ b/exceptions/output/05-finally-trap.txt
@@ -0,0 +1 @@
+swallowed() returned -1 - the IllegalStateException never reached the caller
diff --git a/exceptions/output/06-lambda-checked-compile-error.txt b/exceptions/output/06-lambda-checked-compile-error.txt
new file mode 100644
index 0000000..bcc09ef
--- /dev/null
+++ b/exceptions/output/06-lambda-checked-compile-error.txt
@@ -0,0 +1,4 @@
+src/broken/LambdaChecked.java:8: error: unreported exception IOException; must be caught or declared to be thrown
+ Stream.of("a", "b").map(n -> read(n)).forEach(System.out::println);
+ ^
+1 error
diff --git a/exceptions/output/07-UnreachableCatch-compile-error.txt b/exceptions/output/07-UnreachableCatch-compile-error.txt
new file mode 100644
index 0000000..d0da241
--- /dev/null
+++ b/exceptions/output/07-UnreachableCatch-compile-error.txt
@@ -0,0 +1,4 @@
+src/broken/UnreachableCatch.java:8: error: exception IOException is never thrown in body of corresponding try statement
+ } catch (IOException e) {
+ ^
+1 error
diff --git a/exceptions/output/08-MultiCatchSubclass-compile-error.txt b/exceptions/output/08-MultiCatchSubclass-compile-error.txt
new file mode 100644
index 0000000..c8e02b1
--- /dev/null
+++ b/exceptions/output/08-MultiCatchSubclass-compile-error.txt
@@ -0,0 +1,5 @@
+src/broken/MultiCatchSubclass.java:8: error: Alternatives in a multi-catch statement cannot be related by subclassing
+ try { f(); } catch (FileNotFoundException | IOException e) { }
+ ^
+ Alternative FileNotFoundException is a subclass of alternative IOException
+1 error
diff --git a/exceptions/output/09-WiderOverride-compile-error.txt b/exceptions/output/09-WiderOverride-compile-error.txt
new file mode 100644
index 0000000..859245d
--- /dev/null
+++ b/exceptions/output/09-WiderOverride-compile-error.txt
@@ -0,0 +1,5 @@
+src/broken/WiderOverride.java:7: error: run() in Impl cannot implement run() in Task
+ public void run() throws IOException { throw new IOException(); }
+ ^
+ overridden method does not throw IOException
+1 error
diff --git a/exceptions/output/10-omit-stack-trace-in-fast-throw.txt b/exceptions/output/10-omit-stack-trace-in-fast-throw.txt
new file mode 100644
index 0000000..2cc4917
--- /dev/null
+++ b/exceptions/output/10-omit-stack-trace-in-fast-throw.txt
@@ -0,0 +1,5 @@
+$ java OmitStackTraceDemo
+the ArithmeticException at iteration 1127 had an EMPTY stack trace
+
+$ java -XX:-OmitStackTraceInFastThrow OmitStackTraceDemo
+every ArithmeticException still carried a stack trace in 300000 throws
diff --git a/exceptions/output/11-jmh-raw.txt b/exceptions/output/11-jmh-raw.txt
new file mode 100644
index 0000000..ed55e2f
--- /dev/null
+++ b/exceptions/output/11-jmh-raw.txt
@@ -0,0 +1,13 @@
+Benchmark (depth) Mode Cnt Score Error Units
+ExceptionCostBenchmark.noStackTrace 1 avgt 5 9.810 ± 2.142 ns/op
+ExceptionCostBenchmark.noStackTrace 10 avgt 5 345.467 ± 24.683 ns/op
+ExceptionCostBenchmark.noStackTrace 50 avgt 5 1610.131 ± 148.390 ns/op
+ExceptionCostBenchmark.normalException 1 avgt 5 989.076 ± 239.352 ns/op
+ExceptionCostBenchmark.normalException 10 avgt 5 1589.303 ± 47.103 ns/op
+ExceptionCostBenchmark.normalException 50 avgt 5 4137.934 ± 462.310 ns/op
+ExceptionCostBenchmark.preallocated 1 avgt 5 3.240 ± 0.190 ns/op
+ExceptionCostBenchmark.preallocated 10 avgt 5 336.605 ± 33.790 ns/op
+ExceptionCostBenchmark.preallocated 50 avgt 5 1578.591 ± 74.969 ns/op
+ExceptionCostBenchmark.returnCode 1 avgt 5 1.101 ± 0.033 ns/op
+ExceptionCostBenchmark.returnCode 10 avgt 5 10.786 ± 0.301 ns/op
+ExceptionCostBenchmark.returnCode 50 avgt 5 52.932 ± 1.374 ns/op
diff --git a/exceptions/output/12-deep-trace.txt b/exceptions/output/12-deep-trace.txt
new file mode 100644
index 0000000..a3ca68c
--- /dev/null
+++ b/exceptions/output/12-deep-trace.txt
@@ -0,0 +1,2 @@
+frames the program actually descended: more than 1024 (true)
+getStackTrace().length: 1024
diff --git a/exceptions/output/13-tests.txt b/exceptions/output/13-tests.txt
new file mode 100644
index 0000000..ed77b3f
--- /dev/null
+++ b/exceptions/output/13-tests.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.ankurm.exceptions.ExceptionBehaviourTest
+-------------------------------------------------------------------------------
+Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.105 s -- in com.ankurm.exceptions.ExceptionBehaviourTest
diff --git a/exceptions/output/14-jvm-flags.txt b/exceptions/output/14-jvm-flags.txt
new file mode 100644
index 0000000..23b89e1
--- /dev/null
+++ b/exceptions/output/14-jvm-flags.txt
@@ -0,0 +1,3 @@
+ int MaxJavaStackTraceDepth = 1024 {product} {default}
+ bool OmitStackTraceInFastThrow = true {product} {default}
+ bool StackTraceInThrowable = true {product} {default}
diff --git a/exceptions/pom.xml b/exceptions/pom.xml
new file mode 100644
index 0000000..c5c4249
--- /dev/null
+++ b/exceptions/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+
+ com.ankurm
+ java-core-examples
+ 1.0
+
+
+ exceptions
+ exceptions
+ Java exceptions: checked vs unchecked, try-with-resources suppressed exceptions, cause chaining, custom hierarchies, and what stack-trace capture costs (JMH).
+
+
+ 1.37
+
+
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
+
+ benchmarks
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 25
+
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+ shade
+
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+
+
+
+
+
diff --git a/exceptions/scripts/run-all.sh b/exceptions/scripts/run-all.sh
new file mode 100755
index 0000000..dcd69a8
--- /dev/null
+++ b/exceptions/scripts/run-all.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# Regenerates every file in ../output/. Requires JDK25_HOME.
+set -euo pipefail
+[[ -z "${JDK25_HOME:-}" ]] && { echo "JDK25_HOME must be set" >&2; exit 1; }
+cd "$(dirname "$0")/.."
+OUT=output; mkdir -p "$OUT"
+export JAVA_HOME="$JDK25_HOME"
+mvn -q -f ../pom.xml -pl exceptions -am package
+J="$JDK25_HOME/bin/java"
+run() { echo "==> $1"; "$J" -cp target/classes "com.ankurm.exceptions.$1" 2>&1 | grep -v "Picked up" > "$OUT/$2"; }
+run SuppressedDemo 01-suppressed.txt
+run ChainingDemo 02-chaining.txt
+run CheckedVsUncheckedDemo 03-checked-vs-unchecked.txt
+run CustomHierarchyDemo 04-custom-hierarchy.txt
+run FinallyTrapDemo 05-finally-trap.txt
+echo "==> LambdaChecked (expected compile failure)"
+"$JDK25_HOME/bin/javac" -d /tmp/lc src/broken/LambdaChecked.java 2>&1 | grep -v "Picked up" > "$OUT/06-lambda-checked-compile-error.txt" || true
+for b in UnreachableCatch MultiCatchSubclass WiderOverride; do
+ n=$([[ $b == UnreachableCatch ]] && echo 07 || ([[ $b == MultiCatchSubclass ]] && echo 08 || echo 09))
+ "$JDK25_HOME/bin/javac" -d /tmp/lc src/broken/$b.java 2>&1 | grep -v "Picked up" > "$OUT/$n-$b-compile-error.txt" || true
+done
+echo "==> OmitStackTraceDemo (default, then -XX:-OmitStackTraceInFastThrow)"
+{ echo '$ java OmitStackTraceDemo'; "$J" -cp target/classes com.ankurm.exceptions.OmitStackTraceDemo 2>&1 | grep -v "Picked up"
+ echo; echo '$ java -XX:-OmitStackTraceInFastThrow OmitStackTraceDemo'
+ "$J" -XX:-OmitStackTraceInFastThrow -cp target/classes com.ankurm.exceptions.OmitStackTraceDemo 2>&1 | grep -v "Picked up"; } > "$OUT/10-omit-stack-trace-in-fast-throw.txt"
+run DeepTraceDemo 12-deep-trace.txt
+"$J" -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E "OmitStackTraceInFastThrow|MaxJavaStackTraceDepth|StackTraceInThrowable" | sed 's/ */ /g' > "$OUT/14-jvm-flags.txt"
+echo "==> JMH"
+"$J" -jar target/benchmarks.jar ExceptionCostBenchmark -rf text -rff "$OUT/11-jmh-raw.txt" 2>&1 | grep -v "Picked up" > /dev/null
+cp target/surefire-reports/com.ankurm.exceptions.ExceptionBehaviourTest.txt "$OUT/13-tests.txt"
+echo Done
diff --git a/exceptions/src/broken/LambdaChecked.java b/exceptions/src/broken/LambdaChecked.java
new file mode 100644
index 0000000..4f46369
--- /dev/null
+++ b/exceptions/src/broken/LambdaChecked.java
@@ -0,0 +1,10 @@
+import java.io.IOException;
+import java.util.stream.Stream;
+
+// Deliberately does NOT compile. scripts/run-all.sh captures the real javac message.
+public class LambdaChecked {
+ static String read(String n) throws IOException { return n; }
+ public static void main(String[] args) {
+ Stream.of("a", "b").map(n -> read(n)).forEach(System.out::println);
+ }
+}
diff --git a/exceptions/src/broken/MultiCatchSubclass.java b/exceptions/src/broken/MultiCatchSubclass.java
new file mode 100644
index 0000000..edf2e77
--- /dev/null
+++ b/exceptions/src/broken/MultiCatchSubclass.java
@@ -0,0 +1,10 @@
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// Deliberately does NOT compile: multi-catch alternatives may not be related by subclassing.
+public class MultiCatchSubclass {
+ static void f() throws IOException { throw new FileNotFoundException(); }
+ public static void main(String[] args) {
+ try { f(); } catch (FileNotFoundException | IOException e) { }
+ }
+}
diff --git a/exceptions/src/broken/UnreachableCatch.java b/exceptions/src/broken/UnreachableCatch.java
new file mode 100644
index 0000000..6e90737
--- /dev/null
+++ b/exceptions/src/broken/UnreachableCatch.java
@@ -0,0 +1,12 @@
+import java.io.IOException;
+
+// Deliberately does NOT compile: the try body can never throw IOException.
+public class UnreachableCatch {
+ public static void main(String[] args) {
+ try {
+ System.out.println("hello");
+ } catch (IOException e) {
+ System.out.println("never");
+ }
+ }
+}
diff --git a/exceptions/src/broken/WiderOverride.java b/exceptions/src/broken/WiderOverride.java
new file mode 100644
index 0000000..708dce0
--- /dev/null
+++ b/exceptions/src/broken/WiderOverride.java
@@ -0,0 +1,10 @@
+import java.io.IOException;
+
+// Deliberately does NOT compile: an overriding method may not throw a new checked exception.
+public class WiderOverride {
+ interface Task { void run(); }
+ static class Impl implements Task {
+ public void run() throws IOException { throw new IOException(); }
+ }
+ public static void main(String[] args) { new Impl(); }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/ChainingDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/ChainingDemo.java
new file mode 100644
index 0000000..dea561f
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/ChainingDemo.java
@@ -0,0 +1,33 @@
+package com.ankurm.exceptions;
+
+/** Cause chaining: what "Caused by" and "... N more" mean, and the wrap-without-cause mistake. */
+public class ChainingDemo {
+
+ static void dao() { throw new IllegalStateException("connection reset by peer"); }
+
+ static void serviceGood() {
+ try { dao(); }
+ catch (IllegalStateException e) { throw new RuntimeException("could not load order 42", e); }
+ }
+
+ static void serviceBad() {
+ try { dao(); }
+ catch (IllegalStateException e) { throw new RuntimeException("could not load order 42: " + e.getMessage()); }
+ }
+
+ static void print(String title, Runnable r) {
+ System.out.println("=== " + title + " ===");
+ try { r.run(); }
+ catch (RuntimeException e) {
+ System.out.println("getMessage(): " + e.getMessage());
+ System.out.println("getCause(): " + e.getCause());
+ e.printStackTrace(System.out);
+ }
+ System.out.println();
+ }
+
+ public static void main(String[] args) {
+ print("wrapped WITH the cause", ChainingDemo::serviceGood);
+ print("wrapped WITHOUT the cause (message copied as a string)", ChainingDemo::serviceBad);
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/CheckedVsUncheckedDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/CheckedVsUncheckedDemo.java
new file mode 100644
index 0000000..7f62fd2
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/CheckedVsUncheckedDemo.java
@@ -0,0 +1,46 @@
+package com.ankurm.exceptions;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.List;
+import java.util.stream.Stream;
+
+/** The hierarchy, and the practical consequence of "checked": it does not compose with lambdas. */
+public class CheckedVsUncheckedDemo {
+
+ static void describe(Class> c) {
+ StringBuilder sb = new StringBuilder(c.getSimpleName());
+ for (Class> p = c.getSuperclass(); p != null; p = p.getSuperclass()) sb.append(" -> ").append(p.getSimpleName());
+ boolean checked = Throwable.class.isAssignableFrom(c)
+ && !RuntimeException.class.isAssignableFrom(c) && !Error.class.isAssignableFrom(c);
+ System.out.printf("%-28s %-10s %s%n", c.getSimpleName(), checked ? "CHECKED" : "unchecked", sb);
+ }
+
+ static String read(String name) throws IOException {
+ if (name.startsWith("bad")) throw new IOException("cannot read " + name);
+ return "contents of " + name;
+ }
+
+ public static void main(String[] args) {
+ System.out.println("=== which are checked? ===");
+ for (Class> c : List.of(Exception.class, IOException.class, InterruptedException.class,
+ RuntimeException.class, IllegalArgumentException.class, NullPointerException.class,
+ Error.class, OutOfMemoryError.class, StackOverflowError.class)) {
+ describe(c);
+ }
+
+ System.out.println();
+ System.out.println("=== checked exception inside a stream lambda: wrap in UncheckedIOException ===");
+ try {
+ Stream.of("a.txt", "bad.txt", "c.txt")
+ .map(n -> {
+ try { return read(n); }
+ catch (IOException e) { throw new UncheckedIOException(e); }
+ })
+ .forEach(System.out::println);
+ } catch (UncheckedIOException e) {
+ System.out.println("caught: " + e);
+ System.out.println("original cause: " + e.getCause());
+ }
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/CustomHierarchyDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/CustomHierarchyDemo.java
new file mode 100644
index 0000000..1547f77
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/CustomHierarchyDemo.java
@@ -0,0 +1,48 @@
+package com.ankurm.exceptions;
+
+/**
+ * A small domain hierarchy: one unchecked base type carrying an error code, and subclasses that
+ * callers can catch selectively. Also shows the 4-argument Throwable constructor used in
+ * {@link ControlFlowException}.
+ */
+public class CustomHierarchyDemo {
+
+ /** Base of the application's exceptions. Unchecked: callers that cannot recover need not declare it. */
+ public static class AppException extends RuntimeException {
+ private final String code;
+ public AppException(String code, String message) { super(message); this.code = code; }
+ public AppException(String code, String message, Throwable cause) { super(message, cause); this.code = code; }
+ public String code() { return code; }
+ }
+
+ public static class NotFoundException extends AppException {
+ public NotFoundException(String what, long id) { super("NOT_FOUND", what + " " + id + " does not exist"); }
+ }
+
+ public static class ConflictException extends AppException {
+ public ConflictException(String message, Throwable cause) { super("CONFLICT", message, cause); }
+ }
+
+ /** No stack trace, no suppression: for signalling inside a hot path where the trace would never be read. */
+ public static class ControlFlowException extends RuntimeException {
+ public ControlFlowException(String message) { super(message, null, false, false); }
+ }
+
+ static void handle(Runnable r) {
+ try { r.run(); }
+ catch (NotFoundException e) { System.out.println("404-ish -> " + e.code() + ": " + e.getMessage()); }
+ catch (AppException e) { System.out.println("generic -> " + e.code() + ": " + e.getMessage()); }
+ }
+
+ public static void main(String[] args) {
+ handle(() -> { throw new NotFoundException("order", 42); });
+ handle(() -> { throw new ConflictException("version mismatch", new IllegalStateException("v3 != v4")); });
+
+ ControlFlowException cf = new ControlFlowException("stop early");
+ System.out.println("ControlFlowException stack trace length: " + cf.getStackTrace().length);
+ Exception normal = new RuntimeException("stop early");
+ System.out.println("RuntimeException stack trace length: " + normal.getStackTrace().length);
+ cf.addSuppressed(new RuntimeException("ignored"));
+ System.out.println("ControlFlowException suppressed count after addSuppressed(): " + cf.getSuppressed().length);
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/DeepTraceDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/DeepTraceDemo.java
new file mode 100644
index 0000000..0f3e148
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/DeepTraceDemo.java
@@ -0,0 +1,15 @@
+package com.ankurm.exceptions;
+
+/** MaxJavaStackTraceDepth: a StackOverflowError's trace is cut off at 1024 frames, however deep the stack was. */
+public class DeepTraceDemo {
+ static int depth = 0;
+ static void recurse() { depth++; recurse(); }
+
+ public static void main(String[] args) {
+ try { recurse(); }
+ catch (StackOverflowError e) {
+ System.out.println("frames the program actually descended: more than " + 1024 + " (" + (depth > 1024) + ")");
+ System.out.println("getStackTrace().length: " + e.getStackTrace().length);
+ }
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/ExceptionCostBenchmark.java b/exceptions/src/main/java/com/ankurm/exceptions/ExceptionCostBenchmark.java
new file mode 100644
index 0000000..87e08f6
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/ExceptionCostBenchmark.java
@@ -0,0 +1,59 @@
+package com.ankurm.exceptions;
+
+import org.openjdk.jmh.annotations.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Cost of throw+catch across a call stack of a given depth, for four ways of signalling failure.
+ * Stack depth matters because fillInStackTrace() walks the whole thread stack, not just the frames
+ * between throw and catch.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(1)
+@State(Scope.Thread)
+public class ExceptionCostBenchmark {
+
+ @Param({"1", "10", "50"})
+ public int depth;
+
+ static final class NoTrace extends RuntimeException {
+ NoTrace(String m) { super(m, null, false, false); }
+ }
+ static final NoTrace PREALLOCATED = new NoTrace("preallocated");
+
+ // --- variant 1: ordinary exception, stack trace captured -------------------------------
+ static int throwNormal(int d) {
+ if (d == 0) throw new RuntimeException("boom");
+ return throwNormal(d - 1) + 1;
+ }
+ // --- variant 2: same, writableStackTrace=false ----------------------------------------
+ static int throwNoTrace(int d) {
+ if (d == 0) throw new NoTrace("boom");
+ return throwNoTrace(d - 1) + 1;
+ }
+ // --- variant 3: one shared instance thrown every time ---------------------------------
+ static int throwShared(int d) {
+ if (d == 0) throw PREALLOCATED;
+ return throwShared(d - 1) + 1;
+ }
+ // --- variant 4: no exception, a return code --------------------------------------------
+ static int returnCode(int d) {
+ if (d == 0) return -1;
+ int r = returnCode(d - 1);
+ return r < 0 ? r : r + 1;
+ }
+
+ @Benchmark public int normalException() {
+ try { return throwNormal(depth); } catch (RuntimeException e) { return e.getMessage().length(); }
+ }
+ @Benchmark public int noStackTrace() {
+ try { return throwNoTrace(depth); } catch (RuntimeException e) { return e.getMessage().length(); }
+ }
+ @Benchmark public int preallocated() {
+ try { return throwShared(depth); } catch (RuntimeException e) { return e.getMessage().length(); }
+ }
+ @Benchmark public int returnCode() { return returnCode(depth); }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/FinallyTrapDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/FinallyTrapDemo.java
new file mode 100644
index 0000000..757aa74
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/FinallyTrapDemo.java
@@ -0,0 +1,18 @@
+package com.ankurm.exceptions;
+
+/** A return inside finally silently discards an in-flight exception. */
+@SuppressWarnings("finally")
+public class FinallyTrapDemo {
+
+ static int swallowed() {
+ try {
+ throw new IllegalStateException("important failure");
+ } finally {
+ return -1;
+ }
+ }
+
+ public static void main(String[] args) {
+ System.out.println("swallowed() returned " + swallowed() + " - the IllegalStateException never reached the caller");
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/OmitStackTraceDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/OmitStackTraceDemo.java
new file mode 100644
index 0000000..a15fc52
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/OmitStackTraceDemo.java
@@ -0,0 +1,27 @@
+package com.ankurm.exceptions;
+
+/**
+ * HotSpot's OmitStackTraceInFastThrow: after an implicit exception (NPE, AIOOBE, ArithmeticException...)
+ * has been thrown often enough from JIT-compiled code, the JVM starts reusing a preallocated exception
+ * that has NO stack trace.
+ */
+public class OmitStackTraceDemo {
+
+ static int divide(int a, int b) { return a / b; }
+
+ public static void main(String[] args) {
+ int firstEmpty = -1;
+ for (int i = 1; i <= 300_000; i++) {
+ try {
+ divide(i, 0);
+ } catch (ArithmeticException e) {
+ if (e.getStackTrace().length == 0) { firstEmpty = i; break; }
+ }
+ }
+ if (firstEmpty < 0) {
+ System.out.println("every ArithmeticException still carried a stack trace in 300000 throws");
+ } else {
+ System.out.println("the ArithmeticException at iteration " + firstEmpty + " had an EMPTY stack trace");
+ }
+ }
+}
diff --git a/exceptions/src/main/java/com/ankurm/exceptions/SuppressedDemo.java b/exceptions/src/main/java/com/ankurm/exceptions/SuppressedDemo.java
new file mode 100644
index 0000000..155f4db
--- /dev/null
+++ b/exceptions/src/main/java/com/ankurm/exceptions/SuppressedDemo.java
@@ -0,0 +1,63 @@
+package com.ankurm.exceptions;
+
+/** try-with-resources keeps BOTH exceptions; the hand-written try/finally keeps only the last one. */
+public class SuppressedDemo {
+
+ static class Conn implements AutoCloseable {
+ final String name;
+ Conn(String name) { this.name = name; }
+ void query() { throw new IllegalStateException("query failed on " + name); }
+ @Override public void close() { throw new IllegalArgumentException("close failed on " + name); }
+ }
+
+ public static void main(String[] args) {
+ System.out.println("=== try/finally: the close() exception replaces the query() exception ===");
+ try {
+ Conn c = new Conn("db-1");
+ try {
+ c.query();
+ } finally {
+ c.close();
+ }
+ } catch (Exception e) {
+ System.out.println("caught: " + e);
+ System.out.println("suppressed count: " + e.getSuppressed().length);
+ }
+
+ System.out.println();
+ System.out.println("=== try-with-resources: query() exception wins, close() exception is attached ===");
+ try (Conn c = new Conn("db-1")) {
+ c.query();
+ } catch (Exception e) {
+ System.out.println("caught: " + e);
+ System.out.println("suppressed count: " + e.getSuppressed().length);
+ for (Throwable s : e.getSuppressed()) {
+ System.out.println(" suppressed: " + s);
+ }
+ }
+
+ System.out.println();
+ System.out.println("=== two resources: closed in reverse order, each failure is suppressed ===");
+ try (Conn a = new Conn("A"); Conn b = new Conn("B")) {
+ b.query();
+ } catch (Exception e) {
+ System.out.println("caught: " + e);
+ for (Throwable s : e.getSuppressed()) {
+ System.out.println(" suppressed: " + s);
+ }
+ }
+
+ System.out.println();
+ System.out.println("=== what printStackTrace() shows for suppressed exceptions ===");
+ try (Conn c = new Conn("db-1")) {
+ c.query();
+ } catch (Exception e) {
+ StackTraceElement[] full = e.getStackTrace();
+ e.setStackTrace(new StackTraceElement[] { full[0] }); // keep the transcript short
+ for (Throwable s : e.getSuppressed()) {
+ s.setStackTrace(new StackTraceElement[] { s.getStackTrace()[0] });
+ }
+ e.printStackTrace(System.out);
+ }
+ }
+}
diff --git a/exceptions/src/test/java/com/ankurm/exceptions/ExceptionBehaviourTest.java b/exceptions/src/test/java/com/ankurm/exceptions/ExceptionBehaviourTest.java
new file mode 100644
index 0000000..1c7f382
--- /dev/null
+++ b/exceptions/src/test/java/com/ankurm/exceptions/ExceptionBehaviourTest.java
@@ -0,0 +1,71 @@
+package com.ankurm.exceptions;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+class ExceptionBehaviourTest {
+
+ static class Res implements AutoCloseable {
+ public void close() { throw new IllegalArgumentException("close"); }
+ }
+
+ @Test void tryWithResourcesKeepsPrimaryAndSuppressesClose() {
+ Exception e = assertThrows(IllegalStateException.class, () -> {
+ try (Res r = new Res()) { throw new IllegalStateException("body"); }
+ });
+ assertEquals("body", e.getMessage());
+ assertEquals(1, e.getSuppressed().length);
+ assertEquals("close", e.getSuppressed()[0].getMessage());
+ }
+
+ @Test void tryFinallyLosesTheOriginal() {
+ Exception e = assertThrows(IllegalArgumentException.class, () -> {
+ Res r = new Res();
+ try { throw new IllegalStateException("body"); } finally { r.close(); }
+ });
+ assertEquals("close", e.getMessage());
+ assertEquals(0, e.getSuppressed().length);
+ }
+
+ @Test void writableStackTraceFalseGivesEmptyTraceAndNoSuppression() {
+ var e = new CustomHierarchyDemo.ControlFlowException("x");
+ assertEquals(0, e.getStackTrace().length);
+ e.addSuppressed(new RuntimeException());
+ assertEquals(0, e.getSuppressed().length);
+ assertTrue(new RuntimeException("x").getStackTrace().length > 0);
+ }
+
+ @Test void causeIsPreservedOnlyWhenPassed() {
+ assertNotNull(new RuntimeException("m", new Exception()).getCause());
+ assertNull(new RuntimeException("m").getCause());
+ }
+
+ @Test void returnInFinallySwallowsTheException() {
+ assertEquals(-1, FinallyTrapDemo.swallowed());
+ }
+
+ @Test void classification() {
+ assertTrue(Exception.class.isAssignableFrom(java.io.IOException.class));
+ assertFalse(RuntimeException.class.isAssignableFrom(java.io.IOException.class));
+ assertTrue(RuntimeException.class.isAssignableFrom(UncheckedIOExceptionHolder.type()));
+ }
+
+ @Test void resourcesCloseInReverseOrderAndNullIsSkipped() throws Exception {
+ java.util.List log = new java.util.ArrayList<>();
+ class R implements AutoCloseable {
+ final String n; R(String n) { this.n = n; }
+ public void close() { log.add("close " + n); }
+ }
+ try (R a = new R("A"); R nothing = null; R b = new R("B")) { log.add("body"); }
+ assertEquals(java.util.List.of("body", "close B", "close A"), log);
+ }
+
+ @Test void stackTraceIsCappedAt1024Frames() {
+ try { DeepTraceDemo.main(new String[0]); } catch (Throwable t) { fail(t); }
+ StackOverflowError so = assertThrows(StackOverflowError.class, () -> infinite());
+ assertEquals(1024, so.getStackTrace().length);
+ }
+ static int infinite() { return infinite() + 1; }
+
+ static class UncheckedIOExceptionHolder { static Class> type() { return java.io.UncheckedIOException.class; } }
+}
diff --git a/pom.xml b/pom.xml
index 25868d3..2035d06 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
concurrency-interview
arithmetic
hashmap-concurrenthashmap
+ exceptions