洪 民憙 (Hong Minhee) :nonbinary:'s avatar

洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

1,109 following1,900 followers

An intersectionalist, feminist, and socialist living in Seoul (UTC+09:00). @tokolovesme's spouse. Who's behind @fedify, @hollo, and @botkit. Write some free software in , , , & . They/them.

서울에 사는 交叉女性主義者이자 社會主義者. 金剛兔(@tokolovesme)의 配偶者. @fedify, @hollo, @botkit 메인테이너. , , , 等으로 自由 소프트웨어 만듦.

()

Pinned

@hongminhee@hollo.social

Hello! I'm Hong Minhee (洪 民憙), an open source software engineer in my late 30s, living in Seoul, Korea. I'm bisexual and non-binary (they/them), and an enthusiastic advocate of free/open source software and the fediverse.

I work full-time on @fedify, an ActivityPub server framework in TypeScript, funded by @sovtechfund. I'm also the creator of @hollo, a single-user ActivityPub microblog; @botkit, an ActivityPub bot framework; Hackers' Pub, a fediverse platform for software developers; and LogTape, a logging library for JavaScript and TypeScript.

I have a long interest in East Asian languages (CJK) and Unicode. I post mostly in English here, though occasionally in Japanese or in mixed-script Korean (國漢文混用體), a traditional writing style that interleaves Chinese characters with the native Korean alphabet. Wanting to write in that style was actually one of the reasons I joined the fediverse. Feel free to talk to me in English, Korean, Japanese, or even Literary Chinese!

en.wikipedia.org

Korean mixed script - Wikipedia

Pinned

はじめまして!ソウル在住の30代後半のオープンソースソフトウェアエンジニア、洪 民憙ホン・ミンヒと申します。バイセクシュアル(bisexual)・ノンバイナリー(non-binary)で、自由・オープンソースソフトウェア(F/OSS)とフェディバース(fediverse)の熱烈な支持者です。

STF(@sovtechfund)の支援を受け、TypeScript用ActivityPubサーバーフレームワーク「@fedify」の開発に専念しています。他にも、おひとり様向けのActivityPubマイクロブログ「@hollo」、ActivityPubボットフレームワーク「@botkit」、ソフトウェア開発者向けフェディバースプラットフォームHackers' Pub、JavaScript・TypeScript用ロギングライブラリLogTapeなどの制作者でもあります。

東アジア言語(いわゆるCJK)とUnicodeにも興味があります。このアカウントでは主に英語で投稿していますが、時々日本語や国漢文混用体(漢字ハングル混じり文)の韓国語でも書いています。実はこの文体で書きたくてフェディバースを始めた、という経緯もあります。日本語、英語、韓国語、漢文でも気軽に話しかけてください!

speakerdeck.com

国漢文混用体からHolloまで

本発表では、韓国語の「国漢文混用体」(漢字ハングル混じり文)を自分のフェディバース投稿に実装したいという小さな目標から始まった旅路を共有します。 この目標を達成するために、ActivityPubのJSON-LDの複雑さやHTTP Signatures、WebFingerなどの仕様を理解する必要性に…

Pinned

安寧(안녕)하세요! 저는 서울에 살고 있는 30() 後半(후반)의 오픈 소스 소프트웨어 엔지니어 洪民憙(홍민희)입니다. 兩性愛者(양성애자)(bisexual)이자 논바이너리(non-binary)이며, 自由(자유)·오픈 소스 소프트웨어(F/OSS)와 聯合宇宙(연합우주)(fediverse)의 熱烈(열렬)支持者(지지자)이기도 합니다.

STF(@sovtechfund)의 支援(지원)을 받아 TypeScript() ActivityPub 서버 프레임워크 @fedify 開發(개발)專業(전업)으로 ()하고 있습니다. 그 ()에도 싱글 유저() ActivityPub 마이크로블로그 @hollo, ActivityPub 봇 프레임워크 @botkit, 소프트웨어 開發者(개발자)를 위한 聯合宇宙(연합우주) 플랫폼 Hackers' Pub, JavaScript·TypeScript() 로깅 라이브러리 LogTape ()製作者(제작자)이기도 합니다.

()아시아 言語(언어)(이른바 CJK)와 Unicode에도 關心(관심)이 많습니다. 이 計定(계정)에서는 ()英語(영어)로 포스팅하지만, 때때로 日本語(일본어)國漢文混用體(국한문 혼용체) 韓國語(한국어)로도 씁니다. 聯合宇宙(연합우주)에 오게 된 動機(동기) () 하나가 바로 國漢文混用體(국한문 혼용체)로 글을 쓰고 싶었기 때문이기도 하고요. 韓國語(한국어), 英語(영어), 日本語(일본어), 아니면 漢文(한문)으로도 말을 걸어주세요!

logtape.org

LogTape

Unobtrusive logging library with zero dependencies—library-first design for Deno, Node.js, Bun, browsers, and edge functions

@iamuhun@hackers.pub

해커펍은 퍼머링크로 아카이빙 참조하기 최적이라 생각해서 앞으로 기술을 다루며 기록 및 참조하는 용도로 잘 사용하려고 합니다.

트위터는 나중에 다른 사람에게 보여줄 참조용으로 쓰기에는 너무 정보 대비 소음이 많은 특성 때문에 잘 맞지 않는다고 생각합니다.

@hongminhee@hackers.pub · Reply to 洪 民憙 (Hong Minhee)

📢 Hackers' Pub 招待システムオープン!

Hackers' Pub に招待システムが適用されました。これで設定→招待ページから知人を招待できます。

主な内容:

  • 招待状3枚支給:既存会員の皆様には3枚の招待状が支給されました。
  • 招待方法:設定→招待ページで招待リンクを作成して共有するか、メールアドレスを入力して招待できます。
  • 追加招待:招待状は今後不定期に追加される予定です。
  • 自動フォロー:招待者と被招待者は自動的に相互フォローされます。(フォロー解除可能)

Hackers' Pub のクオリティを維持し、より豊かな技術議論のために慎重な招待をお願いいたします。

ご不明な点やご要望は、この投稿への返信としてお寄せください。

Hackers' Pub コミュニティの成長にご協力をお願いいたします!

Hackers' Pub ウェブサイトの設定メニューにある「招待 (3)」が選択された状態のスクリーンショットです。ページ上部には「友達を Hackers' Pub に招待しましょう。最大 3 人まで招待できます」というテキストが表示されています。その下には、「メールアドレス」とラベル付けされた入力フィールドがあり、「メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます」という説明文が続いています。さらに下には、「追加メッセージ」というラベルの付いたテキストエリアと、「友達は招待メールでこのメッセージを見ることができます」という説明があります。ページ下部には「招待状を送る」というボタンと、「招待者」というセクションがあり、「洪 民意 (Hong Minhee) (@hongminhee@hackers.pub)」という名前とユーザー名が表示されています。
ALT text

Hackers' Pub ウェブサイトの設定メニューにある「招待 (3)」が選択された状態のスクリーンショットです。ページ上部には「友達を Hackers' Pub に招待しましょう。最大 3 人まで招待できます」というテキストが表示されています。その下には、「メールアドレス」とラベル付けされた入力フィールドがあり、「メールアドレスは招待状を受け取るだけでなく、アカウントへのログインにも使用されます」という説明文が続いています。さらに下には、「追加メッセージ」というラベルの付いたテキストエリアと、「友達は招待メールでこのメッセージを見ることができます」という説明があります。ページ下部には「招待状を送る」というボタンと、「招待者」というセクションがあり、「洪 民意 (Hong Minhee) (@hongminhee@hackers.pub)」という名前とユーザー名が表示されています。

@hongminhee@hackers.pub

📢 Hackers' Pub 초대 시스템 오픈!

Hackers' Pub에 초대 시스템이 적용되었습니다. 이제 설정초대 페이지에서 지인들을 초대할 수 있습니다.

주요 내용:

  • 초대장 3장 지급: 기존 회원분들께 3장의 초대장이 지급되었습니다.
  • 초대 방법: 설정 → 초대 페이지에서 초대 링크를 생성하여 공유하거나, 이메일 주소를 입력하여 초대할 수 있습니다.
  • 추가 초대: 초대장은 향후 비정기적으로 추가될 예정입니다.
  • 자동 팔로: 초대자와 피초대자는 자동으로 상호 팔로됩니다. (언팔로 가능.)

Hackers' Pub의 퀄리티를 유지하고, 더욱 풍성한 기술 논의를 위해 신중한 초대를 부탁드립니다.

궁금한 점이나 건의사항은 답글로 남겨주세요.

Hackers' Pub 커뮤니티 성장에 많은 참여 부탁드립니다!

Hackers' Pub 웹사이트의 설정 메뉴에서 “초대 (3)”이 선택된 화면입니다. 페이지 제목은 “Hackers' Pub에 친구를 초대하세요. 현재 3장의 초대장이 남아 있습니다”로 표시되어 있습니다. 아래에는 이메일 주소를 입력하는 필드와 “이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인 할 때도 사용됩니다”라는 안내 문구가 있습니다. 그 아래에는 “추가 메시지”라는 제목의 텍스트 영역과 “초대장을 받는 친구가 볼 수 있는 메시지입니다”라는 설명이 있습니다. 하단에는 “초대장 보내기” 버튼과 “초대한 사람” 목록이 표시되어 있으며, “洪 民意 (Hong Minhee) (@hongminhee@hackers.pub)”라는 이름과 아이디가 적혀 있습니다.
ALT text

Hackers' Pub 웹사이트의 설정 메뉴에서 “초대 (3)”이 선택된 화면입니다. 페이지 제목은 “Hackers' Pub에 친구를 초대하세요. 현재 3장의 초대장이 남아 있습니다”로 표시되어 있습니다. 아래에는 이메일 주소를 입력하는 필드와 “이메일 주소는 초대장을 받을 때 뿐만 아니라, 계정에 로그인 할 때도 사용됩니다”라는 안내 문구가 있습니다. 그 아래에는 “추가 메시지”라는 제목의 텍스트 영역과 “초대장을 받는 친구가 볼 수 있는 메시지입니다”라는 설명이 있습니다. 하단에는 “초대장 보내기” 버튼과 “초대한 사람” 목록이 표시되어 있으며, “洪 民意 (Hong Minhee) (@hongminhee@hackers.pub)”라는 이름과 아이디가 적혀 있습니다.

@hongminhee@hollo.social
@hongminhee@hackers.pub · Reply to 洪 民憙 (Hong Minhee)

Hollo에서도 잘 표시되는 반면, Mastodon에서는 역시나 제대로 표시되지 않네요… 😇

Hackers' Pub에서 입력한 TeX 수식이 Hollo에서도 잘 표시되는 모습
ALT text

Hackers' Pub에서 입력한 TeX 수식이 Hollo에서도 잘 표시되는 모습

Hackers' Pub에서 입력한 TeX 수식이 Mastodon에서는 제대로 표시되지 않는 모습
ALT text

Hackers' Pub에서 입력한 TeX 수식이 Mastodon에서는 제대로 표시되지 않는 모습

@hongminhee@hackers.pub

Hackers' Pub의 숨겨진 기능 중 하나. Markdown에서 TeX 수식을 쓸 수 있습니다. 다음과 같이 $ 사이에 TeX 수식을 넣으면 됩니다.

수식 테스트: $V_{sphere} = \frac{4}{3}\pi r^3$

아래처럼 표시됩니다.

수식 테스트: Vsphere=43πr3V_{sphere} = \frac{4}{3}\pi r^3

@kodingwarrior@hackers.pub
@hongminhee@hollo.social · Reply to charlag

@charlag I think you've misunderstood my point. I wasn't arguing that checked exceptions are better than Result<T, E> types or that explicit propagation is bad. I was suggesting that within Java's ecosystem, checked exceptions could be valuable if properly improved, rather than abandoned entirely. Both approaches have their merits in their respective languages and paradigms. Monadic types definitely compose better, but automatic propagation has advantages in certain contexts too.

@bgl@hackers.pub · Reply to 티르

@tirr 저도 서브타이핑 기반인 TS에 상대적으로 쉽게 도입할 기능이 https://github.com/microsoft/TypeScript/issues/13219 이렇게 오랫동안 진행안되는게 불만입니다. 막상 TS 이펙트 라이브러리들은 |로 흉내내서 잘 쓰고 있더라고요. Haskell처럼 대수적 이펙트는 구현할수있지만 서브타이핑 기반은 아닌 언어에선, 서브타이핑 흉내낸다고 타입레벨 차력쇼하고 있는데 맞는 방향인지 모르겠습니다.

github.com

Suggestion: `throws` clause and typed catch clause · Issue #13219 · microsoft/TypeScript

The typescript type system is helpful in most cases, but it can’t be utilized when handling exceptions. For example: function fn(num: number): void { if (num === 0) { throw "error: can't deal with ...

@tirr@mitir.social

그 뭐라고 하더라... 대수적 이펙트?

RE:
https://hollo.social/@hongminhee/0195b690-cf8e-76b0-84bc-470c9b1c3d5b

hollo.social

Hot take: Despite their bad re…

Hot take: Despite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's `Result<T, E>` or Haskell's `Either a b`—we've been dismissing one of Java's best features all along.

@hongminhee@hollo.social

Hot take: Despite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's Result<T, E> or Haskell's Either a b—we've been dismissing one of Java's best features all along.

@geeknews_bot@sns.lemondouble.com

자바의 체크드 예외 재고찰: 저평가된 타입 안전성 기능
------------------------------
## 주요 내용 요약

* 자바의 체크드 예외가 커뮤니티에서 널리 비판받는 기능임에도 타입 안전성 측면에서 뛰어난 장점 보유.
* Rust의
Result<T, E>나 Haskell의 Either a b와 개념적으로 유사한 타입 안전성 메커니즘 제공.
* 체크드 예외가 메서드 시그니처에 잠재적 실패 가능성을 명시적으로 표현하…
------------------------------
https://news.hada.io/topic?id=19877&utm_source=googlechat&utm_medium=bot&utm_campaign=1834

news.hada.io

자바의 체크드 예외 재고찰: 저평가된 타입 안전성 기능 | GeekNews

주요 내용 요약자바의 체크드 예외가 커뮤니티에서 널리 비판받는 기능임에도 타입 안전성 측면에서 뛰어난 장점 보유.Rust의 Result<T, E>나 Haskell의 Either a b와 개념적으로 유사한 타입 안전성 메커니즘 제공.체크드 예외가 메서드 시그니처에 잠재적 실패 가능성을 명시적으로 표현하여 타입 시스템을 통한 오류 처리 강제.체크드 예외의 장점

hackers.pub

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

Despite 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.

@hongminhee@hackers.pub

Despite their bad reputation in the Java community, checked exceptions provide superior type safety comparable to Rust's Result<T, E> or Haskell's Either a b—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 it
public void readFile(String path) throws IOException {
    Files.readAllLines(Path.of(path));
}

// Unchecked exception - no compiler enforcement
public 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:

  1. Handle the exception with a try-catch block
  2. Propagate 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 call
fn 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 declared
void 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 exceptions
List<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 inline
List<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 exceptions
List<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:

@FunctionalInterface
public 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 streams
List<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 API
List<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 API
Optional<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.Try

val 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.

@hongminhee@hollo.social · Reply to bgl gwyng

@bgl Absolutely! You make a great point about subtyping—it's one of Java's underappreciated strengths for error handling. The exception hierarchy lets you catch specific exceptions or their broader subtypes as needed, giving you flexible error composition without extra syntax.