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