exceptions: Java exception handling companion code (suppression, chaining, cost)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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; } }
|
||||
}
|
||||
Reference in New Issue
Block a user