writing.exchange is one of the many independent Mastodon servers you can use to participate in the fediverse.
A small, intentional community for poets, authors, and every kind of writer.

Administered by:

Server stats:

335
active users

#result

0 posts0 participants0 posts today

Revisiting Java's Checked Exceptions: An Underappreciated Type Safety Feature

hackers.pub/@hongminhee/2025/r

Hackers' Pub · Revisiting Java's Checked Exceptions: An Underappreciated Type Safety FeatureDespite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's Result<T, E> or Haskell's Either—we've been dismissing one of Java's best features all along.Introduction Few features in Java have been as consistently criticized as checked exceptions. Modern Java libraries and frameworks often go to great lengths to avoid them. Newer JVM languages like Kotlin have abandoned them entirely. Many experienced Java developers consider them a design mistake. But what if this conventional wisdom is wrong? What if checked exceptions represent one of Java's most forward-thinking features? In this post, I'll argue that Java's checked exceptions were ahead of their time, offering many of the same type safety benefits that are now celebrated in languages like Rust and Haskell. Rather than abandoning this feature, we should consider how to improve it to work better with modern Java's features.Understanding Java's Exception Handling Model To set the stage, let's review how Java's exception system works:<Unchecked exceptions (subclasses of RuntimeException or Error): These don't need to be declared or caught. They typically represent programming errors (NullPointerException, IndexOutOfBoundsException) or unrecoverable conditions (OutOfMemoryError).<Checked exceptions (subclasses of Exception but not RuntimeException): These must either be caught with try/catch blocks or declared in the method signature with throws. They represent recoverable conditions that are outside the normal flow of execution (IOException, SQLException).<Here's how this works in practice:// Checked exception - compiler forces you to handle or declare itpublic void readFile(String path) throws IOException { Files.readAllLines(Path.of(path));}// Unchecked exception - no compiler enforcementpublic void processArray(int[] array) { int value = array[array.length + 1]; // May throw ArrayIndexOutOfBoundsException}<The Type Safety Argument for Checked Exceptions At their core, checked exceptions are a way of encoding potential failure modes into the type system via method signatures. This makes certain failure cases part of the API contract, forcing client code to explicitly handle these cases. Consider this method signature:public byte[] readFileContents(String filePath) throws IOException<The throws IOException clause tells us something critical: this method might fail in ways related to IO operations. The compiler ensures you can't simply ignore this fact. You must either:Handle the exception with a try-catch blockPropagate it by declaring it in your own method signature<This type-level representation of potential failures aligns perfectly with principles of modern type-safe programming.Automatic Propagation: A Hidden Advantage One often overlooked advantage of Java's checked exceptions is their automatic propagation. Once you declare a method as throws IOException, any exception that occurs is automatically propagated to the caller without additional syntax. Compare this with Rust, where you must use the ? operator every time you call a function that returns a Result:// Rust requires explicit propagation with ? for each callfn read_and_process(path: &str) -> Result<(), std::io::Error> { let content = std::fs::read_to_string(path)?; process_content(&content)?; Ok(())}// Java automatically propagates exceptions once declaredvoid readAndProcess(String path) throws IOException { String content = Files.readString(Path.of(path)); processContent(content); // If this throws IOException, it's automatically propagated}<In complex methods with many potential failure points, Java's approach leads to cleaner code by eliminating the need for repetitive error propagation markers.Modern Parallels: Result Types in Rust and Haskell The approach of encoding failure possibilities in the type system has been adopted by many modern languages, most notably Rust with its Result<T, E> type and Haskell with its Either a b type. In Rust:fn read_file_contents(file_path: &str) -> Result<Vec<u8>, std::io::Error> { std::fs::read(file_path)}<When calling this function, you can't just ignore the potential for errors—you need to handle both the success case and the error case, often using the ? operator or pattern matching. In Haskell:readFileContents :: FilePath -> IO (Either IOException ByteString)readFileContents path = try $ BS.readFile path<Again, the caller must explicitly deal with both possible outcomes. This is fundamentally the same insight that motivated Java's checked exceptions: make failure handling explicit in the type system.Valid Criticisms of Checked Exceptions If checked exceptions are conceptually similar to these widely-praised error handling mechanisms, why have they fallen out of favor? There are several legitimate criticisms:1. Excessive Boilerplate in the Call Chain The most common complaint is the boilerplate required when propagating exceptions up the call stack:void methodA() throws IOException { methodB();}void methodB() throws IOException { methodC();}void methodC() throws IOException { // Actual code that might throw IOException}<Every method in the chain must declare the same exception, creating repetitive code. While automatic propagation works well within a method, the explicit declaration in method signatures creates overhead.2. Poor Integration with Functional Programming Java 8 introduced lambdas and streams, but checked exceptions don't play well with them:// Won't compile because map doesn't expect functions that throw checked exceptionsList<String> fileContents = filePaths.stream() .map(path -> Files.readString(Path.of(path))) // Throws IOException .collect(Collectors.toList());<This forces developers to use awkward workarounds:List<String> fileContents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new UncheckedIOException(e); // Wrap in an unchecked exception } }) .collect(Collectors.toList());<3. Interface Evolution Problems Adding a checked exception to an existing method breaks all implementing classes and calling code. This makes evolving interfaces over time difficult, especially for widely-used libraries and frameworks.4. Catch-and-Ignore Anti-Pattern The strictness of checked exceptions can lead to the worst possible outcome—developers simply catching and ignoring exceptions to make the compiler happy:try { // Code that might throw} catch (Exception e) { // Do nothing or just log}<This is worse than having no exception checking at all because it provides a false sense of security.Improving Checked Exceptions Without Abandoning Them Rather than abandoning checked exceptions entirely, Java could enhance the existing system to address these legitimate concerns. Here are some potential improvements that preserve the type safety benefits while addressing the practical problems:1. Allow lambdas to declare checked exceptions One of the biggest pain points with checked exceptions today is their incompatibility with functional interfaces. Consider how much cleaner this would be:// Current approach - forced to handle or wrap exceptions inlineList<String> contents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList());// Potential future approach - lambdas can declare exceptionsList<String> contents = filePaths.stream() .map((String path) throws IOException -> Files.readString(Path.of(path))) .collect(Collectors.toList());<This would require updating functional interfaces to support exception declarations:@FunctionalInterfacepublic interface Function<T, R, E extends Exception> { R apply(T t) throws E;}<2. Generic exception types in throws clauses Another powerful enhancement would be allowing generic type parameters in throws clauses:public <E extends Exception> void processWithException(Supplier<Void, E> supplier) throws E { supplier.get();}<This would enable much more flexible composition of methods that work with different exception types, bringing some of the flexibility of Rust's Result<T, E> to Java's existing exception system.3. Better support for exception handling in functional contexts Unlike Rust which requires the ? operator for error propagation, Java already automatically propagates checked exceptions when declared in the method signature. What Java needs instead is better support for checked exceptions in functional contexts:// Current approach for handling exceptions in streamsList<String> contents = filePaths.stream() .map(path -> { try { return Files.readString(Path.of(path)); } catch (IOException e) { throw new RuntimeException(e); // Lose type information } }) .collect(Collectors.toList());// Hypothetical improved APIList<String> contents = filePaths.stream() .mapThrowing(path -> Files.readString(Path.of(path))) // Preserves checked exception .onException(IOException.class, e -> logError(e)) .collect(Collectors.toList());<4. Integration with Optional<T> and Stream<T> APIs The standard library could be enhanced to better support operations that might throw checked exceptions:// Hypothetical APIOptional<String> content = Optional.ofThrowable(() -> Files.readString(Path.of("file.txt")));content.ifPresentOrElse( this::processContent, exception -> log.error("Failed to read file", exception));<Comparison with Other Languages' Approaches It's worth examining how other languages have addressed the error handling problem:Rust's Result<T, E> and ? operator Rust's approach using Result<T, E> and the ? operator shows how propagation can be made concise while keeping the type safety benefits. The ? operator automatically unwraps a successful result or returns the error to the caller, making propagation more elegant. However, Rust's approach requires explicit propagation at each step, which can be more verbose than Java's automatic propagation in certain scenarios.Kotlin's Approach Kotlin made all exceptions unchecked but provides functional constructs like runCatching that bring back some type safety in a more modern way:val result = runCatching { Files.readString(Path.of("file.txt"))}result.fold( onSuccess = { content -> processContent(content) }, onFailure = { exception -> log.error("Failed to read file", exception) })<This approach works well with Kotlin's functional programming paradigm but lacks compile-time enforcement.Scala's Try[T], Either[A, B], and Effect Systems Scala offers Try[T], Either[A, B], and various effect systems that encode errors in the type system while integrating well with functional programming:import scala.util.Tryval fileContent: Try[String] = Try { Source.fromFile("file.txt").mkString}fileContent match { case Success(content) => processContent(content) case Failure(exception) => log.error("Failed to read file", exception)}<This approach preserves type safety while fitting well with Scala's functional paradigm.Conclusion Java's checked exceptions were a pioneering attempt to bring type safety to error handling. While the implementation has shortcomings, the core concept aligns with modern type-safe approaches to error handling in languages like Rust and Haskell. Copying Rust's Result<T, E> might seem like the obvious solution, but it would represent a radical departure from Java's established paradigms. Instead, targeted enhancements to the existing checked exceptions system—like allowing lambdas to declare exceptions and supporting generic exception types—could preserve Java's unique approach while addressing its practical limitations. The beauty of such improvements is that they'd maintain backward compatibility while making checked exceptions work seamlessly with modern Java features like lambdas and streams. They would acknowledge that the core concept of checked exceptions was sound—the problem was in the implementation details and their interaction with newer language features. So rather than abandoning checked exceptions entirely, perhaps we should recognize them as a forward-thinking feature that was implemented before its time. As Java continues to evolve, we have an opportunity to refine this system rather than replace it. In the meantime, next time you're tempted to disparage checked exceptions, remember: they're not just an annoying Java quirk—they're an early attempt at the same type safety paradigm that newer languages now implement with much celebration. What do you think? Could these improvements make checked exceptions viable for modern Java development? Or is it too late to salvage this controversial feature? I'm interested in hearing your thoughts in the comments.

#result : the conclusion or end to which any course or condition of things leads, or which is obtained by any process or operation

- French: un résultat

- German: das Ergebnis

- Italian: risultato

- Portuguese: resultado

- Spanish: resultado

------------

See previous words @ wordofthehour.org/r/past

wordofthehour.orgWord of The HourWord of the Hour helps expand your vocabulary. Every hour, a new vocabulary word is featured along with translations into 10+ languages including French, German, Hindi, Italian, Portuguese, Spanish, and more. We are supported by our warm and welcoming community on Reddit.

any medical nerds want to peek inside my unhappy fucking blood results, here you go:

"ALT:ALP RATIO: 1.0 and
Normal range: 0.0 to 1.0 .

Serum ALT level: 79 iu/L and
Normal range: 7 to 40 .

Total white cell count: 19.9 10 * 9/L and
Normal range: 4.0 to 11.0 .

Mean corpuscular volume (MCV): 100.7 fL and
Normal range: 76.0 to 100.0 .

Mean corpusc. haemoglobin(MCH): 32.7 pg and
Normal range: 27.0 to 32.0 .

Neutrophil count: 16.1 10 * 9/L and
Normal range: 2.0 to 7.5 .

Monocyte count: 1.4 10 * 9/L and
Normal range: 0.2 to 0.8 ."

the rest were all within bounds.

Nice just worked out how to convert my ssh-askpass bash script to a posix sh script ssh-askpass.sh.

In bash it was :

#!/usr/bin/env bash
# Remember to export SSH_ASKPASS=~/.local/bin/ssh-askpass in your .bashrc
RESULT=$(pinentry-curses --ttytype=xterm-color --lc-ctype=en_US.UTF8 --ttyname=/dev/tty <<END | grep -E '^(D|ERR)'
SETDESC Enter your SSH password:
SETPROMPT
GETPIN
END
)

if [ "$RESULT" == "ERR 111 canceled" ]; then
exit 255
else
echo ${RESULT:2:${#RESULT}-2}
fi
In sh it is:

#!/bin/sh
# Remember to export SSH_ASKPASS=~/.local/bin/ssh-askpass.sh
RESULT=$(pinentry-curses --ttytype=xterm-color --lc-ctype=en_US.UTF8 --ttyname=/dev/tty <<END | grep -E '^(D|ERR)'
SETDESC Enter your SSH password:
SETPROMPT
GETPIN
END
)

if [ "$RESULT" = "ERR 111 canceled" ]; then
exit 255
else
length=$(printf "%s" "$RESULT" | wc -c)
length=$((length))
echo "$RESULT" : '..' | cut -c 3-"$length"
fi
It might not be perfect but it works for me. #RunBSD #FreeBSD #Posix