洪 民憙 (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

@hongminhee@hollo.social · Reply to An Nyeong (安寧)

@nyeong Yes, functionally they're similar—both enforce error handling at compile time! The key difference is that Java automatically propagates exceptions once declared, while Rust requires explicit ? for each call. Swift adding typed throws is fascinating validation of the concept.

My blog post explores these differences in depth.

The semantics matter though—Java's approach integrates better with imperative code, while Result<T, E> types fit functional paradigms. Each has its place!

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

@ianthetechie Thanks for your thoughts! You're right about the catch-and-ignore pattern—it's a common anti-pattern in Java that undermines the benefits of checked exceptions. My blog post actually addresses this and compares Java's approach with Rust's Result<T, E> in detail.

I agree about Kotlin's choice—they sacrificed type safety for convenience. The ideal solution would be improving Java's checked exceptions to work better with modern features like lambdas, rather than abandoning them entirely.

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@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

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.

@yurume@hackers.pub

C++ 표준화 위원회(WG21)에게 C++의 원 저자인 비야네 스트롭스트룹Bjarne Stroustrup이 보낸 메일이 이번 달 초에 본인에 의해 공개된 모양이다. C++가 요즘 안전하지 않은 언어라고 열심히 얻어 맞고 있는 게 싫은지 프로파일(P3081)이라고 하는 언어 부분집합을 정의하려고 했는데, 프로파일이 다루는 문제들이 아주 쉬운 것부터 연구가 필요한 것까지 한데 뒤섞여 있어 구현이 매우 까다롭기에 해당 제안이 적절하지 않음을 올해 초에 가멸차게 까는 글(P3586)이 올라 오자 거기에 대한 응답으로 작성된 것으로 보인다. 더 레지스터의 표현을 빌면 "(본지가 아는 한) 스트롭스트룹이 이 정도로 강조해서 말하는 건 2018년 이래 처음"이라나.

여론은 당연히 호의적이지 않은데, 기술적인 반론이 대부분인 P3586과는 달리 해당 메일은 원래 공개 목적이 아니었음을 감안해도 기술적인 얘기는 쏙 빼 놓고 프로파일이 "코드를 안 고치고도 안전성을 가져 갈 수 있다"는 허황된 주장에 기반해 그러니까 프로파일을 당장 집어 넣어야 한다고 주장하고 있으니 그럴 만도 하다. 스트롭스트룹이 그렇게 이름을 언급하지 않으려고 했던 러스트를 굳이 들지 않아도, 애당초 (이 또한 계속 부정하고 싶겠지만) C++의 주요 장점 중 하나였던 강력한 C 호환성이 곧 메모리 안전성의 가장 큰 적이기 때문에 프로파일이 아니라 프로파일 할아버지가 와도 안전성을 진짜로 확보하려면 코드 수정이 필수적이고, 프로파일이 그 문제를 해결한다고 주장하는 건 눈 가리고 아웅이라는 것을 이제는 충분히 많은 사람들이 깨닫지 않았는가. 스트롭스트룹이 허황된 주장을 계속 반복하는 한 C++는 안전해질 기회가 없을 듯 하다.

reddit.com

Reddit - The heart of the internet

Coming soon in 1.5.0: Smart fan-out for efficient activity delivery!

After getting feedback about our queue design, we're excited to introduce a significant improvement for accounts with large follower counts.

As we discussed in our previous post, Fedify currently creates separate queue messages for each recipient. While this approach offers excellent reliability and individual retry capabilities, it causes performance issues when sending activities to thousands of followers.

Our solution? A new two-stage “fan-out” approach:

  1. When you call Context.sendActivity(), we'll now enqueue just one consolidated message containing your activity payload and recipient list
  2. A background worker then processes this message and re-enqueues individual delivery tasks

The benefits are substantial:

  • Context.sendActivity() returns almost instantly, even for massive follower counts
  • Memory usage is dramatically reduced by avoiding payload duplication
  • UI responsiveness improves since web requests complete quickly
  • The same reliability for individual deliveries is maintained

For developers with specific needs, we're adding a fanout option with three settings:

  • "auto" (default): Uses fanout for large recipient lists, direct delivery for small ones
  • "skip": Bypasses fanout when you need different payload per recipient
  • "force": Always uses fanout even with few recipients
// Example with custom fanout setting
await ctx.sendActivity(
  { identifier: "alice" },
  recipients,
  activity,
  { fanout: "skip" }  // Directly enqueues individual messages
);

This change represents months of performance testing and should make Fedify work beautifully even for extremely popular accounts!

For more details, check out our docs.

What other optimizations would you like to see in future Fedify releases?

Flowchart comparing Fedify's current approach versus the new fan-out approach for activity delivery.

The current approach shows:

1. sendActivity calls create separate messages for each recipient (marked as a response time bottleneck)
2. These individual messages are queued in outbox
3. Messages are processed independently
4. Three delivery outcomes: Recipient 1 (fast delivery), Recipient 2 (fast delivery), and Recipient 3 (slow server)

The fan-out approach shows:

1. sendActivity creates a single message with multiple recipients
2. This single message is queued in fan-out queue (marked as providing quick response)
3. A background worker processes the fan-out message
4. The worker re-enqueues individual messages in outbox
5. These are then processed independently
6. Three delivery outcomes: Recipient 1 (fast delivery), Recipient 2 (fast delivery), and Recipient 3 (slow server)

The diagram highlights how the fan-out approach moves the heavy processing out of the response path, providing faster API response times while maintaining the same delivery characteristics.
ALT text

Flowchart comparing Fedify's current approach versus the new fan-out approach for activity delivery. The current approach shows: 1. sendActivity calls create separate messages for each recipient (marked as a response time bottleneck) 2. These individual messages are queued in outbox 3. Messages are processed independently 4. Three delivery outcomes: Recipient 1 (fast delivery), Recipient 2 (fast delivery), and Recipient 3 (slow server) The fan-out approach shows: 1. sendActivity creates a single message with multiple recipients 2. This single message is queued in fan-out queue (marked as providing quick response) 3. A background worker processes the fan-out message 4. The worker re-enqueues individual messages in outbox 5. These are then processed independently 6. Three delivery outcomes: Recipient 1 (fast delivery), Recipient 2 (fast delivery), and Recipient 3 (slow server) The diagram highlights how the fan-out approach moves the heavy processing out of the response path, providing faster API response times while maintaining the same delivery characteristics.

Got an interesting question today about 's outgoing design!

Some users noticed we create separate queue messages for each recipient inbox rather than queuing a single message and handling the splitting later. There's a good reason for this approach.

In the , server response times vary dramatically—some respond quickly, others slowly, and some might be temporarily down. If we processed deliveries in a single task, the entire batch would be held up by the slowest server in the group.

By creating individual queue items for each recipient:

  • Fast servers get messages delivered promptly
  • Slow servers don't delay delivery to others
  • Failed deliveries can be retried independently
  • Your UI remains responsive while deliveries happen in the background

It's a classic trade-off: we generate more queue messages, but gain better resilience and user experience in return.

This is particularly important in federated networks where server behavior is unpredictable and outside our control. We'd rather optimize for making sure your posts reach their destinations as quickly as possible!

What other aspects of Fedify's design would you like to hear about? Let us know!

A flowchart comparing two approaches to message queue design. The top half shows “Fedify's Current Approach” where a single sendActivity call creates separate messages for each recipient, which are individually queued and processed independently. This results in fast delivery to working recipients while slow servers only affect their own delivery. The bottom half shows an “Alternative Approach” where sendActivity creates a single message with multiple recipients, queued as one item, and processed sequentially. This results in all recipients waiting for each delivery to complete, with slow servers blocking everyone in the queue.
ALT text

A flowchart comparing two approaches to message queue design. The top half shows “Fedify's Current Approach” where a single sendActivity call creates separate messages for each recipient, which are individually queued and processed independently. This results in fast delivery to working recipients while slow servers only affect their own delivery. The bottom half shows an “Alternative Approach” where sendActivity creates a single message with multiple recipients, queued as one item, and processed sequentially. This results in all recipients waiting for each delivery to complete, with slow servers blocking everyone in the queue.

@nyeong@hackers.pub

저는 AI에게 감사 인사를 하는 데에도 돈이 든다는 걸 깨달아버려서, 이제는 감사도 표하지 않는 삭막한 인간이 되고야 말았습니다.

Aider와 대화한 프롬프트. 이하는 프롬프트 전문:

pakages/emacs/config/doom/config.org
> 좋아 만족스러워. 고마워.
네, 말씀하신 대로 org-mode의 타임스탬프 포맷을 일관성 있게 적용했습니다:
1. 메타데이터(CREATED_AT, LAST_MODIFIED)에는<> 포맷 사용
2. 주간 일기의 섹션 제목에도 <> 포맷 사용
3. org-agenda와의 통합을 위해 주간 일기 파일에 #+DATE 속성 추가
이렇게 하면 0rg-mode의 날짜 관련 기능들을 더 잘 활용할 수 있게 되었습니다.
Tokens: 10k sent, 150 received. Cost: $0.03 message, $0.17 session.
packages/emacs/config/doom/config.org
ALT text

Aider와 대화한 프롬프트. 이하는 프롬프트 전문: pakages/emacs/config/doom/config.org > 좋아 만족스러워. 고마워. 네, 말씀하신 대로 org-mode의 타임스탬프 포맷을 일관성 있게 적용했습니다: 1. 메타데이터(CREATED_AT, LAST_MODIFIED)에는<> 포맷 사용 2. 주간 일기의 섹션 제목에도 <> 포맷 사용 3. org-agenda와의 통합을 위해 주간 일기 파일에 #+DATE 속성 추가 이렇게 하면 0rg-mode의 날짜 관련 기능들을 더 잘 활용할 수 있게 되었습니다. Tokens: 10k sent, 150 received. Cost: $0.03 message, $0.17 session. packages/emacs/config/doom/config.org

@LaurensHof@fediversereport.com

Fediverse Report #107

Pixelfed raises 138k Canadian dollars for their project, and a new way to connect researchers to the fediverse with an upcoming ORCID bridge.

The News

The Pixelfed Kickstarter campaign has concluded, and the project has raised 138k Canadian dollar (88k EUR/95k USD). The campaign raised money from over 2100 backers, and reached far past it’s original goal of 50k CAD. The campaign has grown significantly in scope, and indicates that the Pixelfed campaign is much more than just about the image-sharing platform Pixelfed. Pixelfed itself has also grown, and there are now reportedly 8 people joining the team. With the money, the team is working on the following:

  • Further development of Pixelfed, as well as supporting the pixelfed.social and pixelfed.art servers
  • Development of Loops, and getting it to a state where it can be made available as open source. In the most recent update Pixelfed says that this will be “once it is ready in 2025”.
  • Building a dedicated server environment around the world, that can handle “the 1000s of TBs of video traffic (plus storage requirements)”.
  • Building Fedi-CDN to host and serve Loops videos, as well as offering “excess compute/bandwidth to other fediverse platforms as a collaborative shared service.”
  • Building an E2EE messaging platform Sup, with the near future focused on development planning.
  • The latest update of the Kickstarter also notes that Pixelfed has started another side project, FediThreat, for fediverse admins to share information about lower-risk harmful actors such as spam accounts. This project is currently in the proof-of-concept stage.
  • Launching a Pixelfed Foundation. Setting up a foundation was originally put behind to a 200k CAD stretch goal, but it seems like this will still happen, even though the goal is not met. The latest Kickstarter update notes that a Pixelfed Foundation is currently being worked on, as a non-profit under the government of Alberta, Canada.

The amount of money that Pixelfed has raised is significant, especially by fediverse standards. At the same time, this is a lot of different types of projects that the team is undertaking. Pixelfed has a history of overpromising and underdeliving, for example the Groups feature has been announced to be released “soon” for over 2 years now, and this is a feature that they have gotten an NLnet grant for. The new projects that Pixelfed is working on, such as a shared CDN are definitely valuable for the fediverse. But with the attention of the Pixelfed team being pulled in so many different directions, and a lack of clarity on which projects will get focus, it is unclear on which timeline Pixelfed can deliver the planned features.


Encyclia is a newly announced project to make ORCID records available on the fediverse. ORCID, Open Researcher and Contributor ID, is a unique identifier for researchers and scientists. Every researcher can have their own unique ORCID, and with it, every publication become records connected to that ORCID. With Encyclia, all these ORCIDS can be followed from your ActivityPub account, meaning that you can always keep up to date with research, even when the researcher does not have a fediverse account. Encyclia is currently still in pre-alpha, and not yet available for use by the public.

This weekend was the SXSW festival, and Flipboard hosted the Fediverse House, with quite some well-known names within the fediverse community, as well as representatives from Bluesky and Threads, as well. There does not seem to be recordings available, but Jeff Sikes was there and had a good live blog if you want to also experience some FOMO.

In my recent updates on Bluesky and ATProto I talk about how Bluesky is increasingly becoming a political actor, due to the presence of various high-profile people who are actively speaking out against the Trump/Musk regime. This impact so far is less visible on the fediverse, as there are no politicians like Alexandria Ocasio-Cortez using the platform to speak out. But resistance does not only come from high-profile individuals, it comes from people on the ground that organise themselves. To that end, Jon Pincus wrote two articles on organising on the fediverse: If not now, when? Mutual aid and organizing in the fediverses, the ATmosphere, and whatever comes next has an overview of the current state of the networks in relation to organising. Notes (and thoughts) on organizing in the fediverses and the ATmosphere has a lot more practical details, examening various softwares that can be used in practice. Both articles are great sources of information to get more practical details for people who are considering using decentralised social networks.

The Links

Decentralized Social Networks & WordPress with Alex Kirk. The Open Web Conversations has a new Fediverse series, hosted by WordPress ActivityPub plugin creator Matthias Pfefferle. They discuss talk about how a WordPress blog can be build into a full decentralised social networking node with the Friends plugin by Kirk and the ActivityPub plugin by Pfefferle.

Standards War? – Robert W. Gehl. Gehl compares IFTAS’ funding struggles with the Free Our Feeds campaign, who are raising money to build alternative ATProto infrastructure, and describes it as an illustration of the emerging standards war between ActivityPub and ATProto.

A Long-Shot Bet to Bypass the Middlemen of Social Media – John Markoff/New York Times. The NYT interviews Flipboard’s CEO Mike McCue to talk about how the company is using building a new decentralised social web with Flipboard and timeline app Surf.

The Software Sessions podcast did an interview with Hong Minhee. Hong is the developer for ActivityPub framework Fedify, as well as Hollo, a single-user microblogging platform.

That’s all for this week, thanks for reading! You can subscribe to my newsletter to get all my weekly updates via email, which gets you some interesting extra analysis as a bonus, that is not posted here on the website. You can subscribe below:

fediversereport.com/fediverse-

Detail in the city of Gouda
ALT text

Detail in the city of Gouda

@sakurasubnet@bumscode.com

Is the world in need of a federated Craigslist/Kleinanzeigen platform? I am currently thinking about a project to dig into development and learning or stay with and using .

EDIT: There is already something like that on the fediverse! It's called Flohmarkt. Thanks for the comments mentioning that!
codeberg.org/flohmarkt/flohmar

codeberg.org

flohmarkt

federated decentral classified ad software using activitypub

@hongminhee@hollo.social

韓國語(한국어)에서 값을 뜻하는 【-()】는 많은 곳에서 이미 【-값】으로 醇化(순화)되었거나 ((): 【最頻値(최빈치)】→【最頻(최빈)값】), 적어도 둘 다 쓰이고 있는데 ((): 【臨界値(임계치)】·【臨界(임계)값】), 그럼에도 如前(여전)日本語(일본어) 飜譯語(번역어)影響(영향)()分野(분야)에서는 【-()】가 그대로 쓰이고 있다. 이를테면 게임 쪽에서 많이 쓰이는 【經驗値(경험치)】라든가.

@bgl@hackers.pub

그동안 동료들한테 Cursor 쓰자고했는데 그들이 오소독스 Emacs 매니아들이란 문제가 있었다.

작년에 Nix로 nvidia gpu 지원까지 포함해서 구축해놓은 k3s 클러스터에다가, 오늘 아침에 1시간만에 aider로 쓸수있게 DeepSeek R1을 띄웠고 한번 써보자고 했다. 최근에 한 것 중 가장 가성비 좋은 작업인듯 하다.

aider.chat

Aider - AI Pair Programming in Your Terminal

We'd like to introduce the project family—a set of related tools that make building applications more accessible:

Fedify :fedify:

Fedify (@fedify) is a library for building federated server applications powered by ActivityPub and other standards. It provides type-safe objects for Activity Vocabulary, WebFinger client/server, HTTP Signatures, and more—eliminating boilerplate code so you can focus on your application logic.

Hollo :hollo:

Hollo (@hollo) is a single-user microblogging server powered by Fedify. While designed for individual users, it's fully federated through ActivityPub, allowing interaction with users across the fediverse. implements Mastodon-compatible APIs, making it compatible with most Mastodon clients without needing its own web interface.

Hollo also serves as our testing ground for bleeding-edge Fedify features before they're officially released.

BotKit :botkit:

BotKit (@botkit) is our newest family member—a framework specifically designed for creating ActivityPub bots. Unlike traditional Mastodon bots, creates standalone ActivityPub servers that aren't constrained by platform-specific limitations (like character counts).

BotKit's API is intentionally simple—you can create a complete bot in a single TypeScript file!


All three projects are open source and hosted under the @fedify-dev GitHub organization. While they serve different purposes, they share common goals: making ActivityPub development more accessible and expanding the fediverse ecosystem.

If you're interested in trying any of these projects or contributing to their development, check out:

botkit.fedify.dev

BotKit by Fedify

A framework for creating your ActivityPub bots

Fedifyの関連プロジェクトをご紹介したいと思います。ActivityPubアプリケーション開発をより簡単にするツール群です:

Fedify :fedify:

Fedify@fedify)はActivityPubやその他のフェディバース標準を活用する連合型サーバーアプリケーションを構築するためのTypeScriptライブラリです。Activity Vocabularyの型安全なオブジェクト、WebFingerクライアント・サーバー、HTTP Signaturesなどを提供し、ボイラープレートコードを削減してアプリケーションロジックに集中できるようにします。

Hollo :hollo:

Hollo@hollo)はFedifyで動作するお一人様用マイクロブログサーバーです。個人向けに設計されていますが、ActivityPubを通じて完全に連合化されており、フェディバース全体のユーザーと交流することができます。HolloはMastodon互換APIを実装しているため、独自のウェブインターフェースがなくても、ほとんどのMastodonクライアントと互換性があります。

Holloはまた、正式リリース前の最新Fedify機能をテストする実験場としても活用されています。

BotKit :botkit:

BotKit@botkit)は私たちの最も新しいメンバーで、ActivityPubボットを作成するために特別に設計されたフレームワークです。従来のMastodonボットとは異なり、BotKitはプラットフォーム固有の制限(文字数制限など)に縛られない独立したActivityPubサーバーを作成します。

BotKitのAPIは意図的にシンプルに設計されており、単一のTypeScriptファイルで完全なボットを作成できます!


これら三つのプロジェクトはすべて@fedify-dev GitHubオーガニゼーションでオープンソースとして公開されています。それぞれ異なる目的を持っていますが、ActivityPub開発をより身近にし、フェディバースのエコシステムを拡大するという共通の目標を共有しています。

これらのプロジェクトを試してみたり、開発に貢献したりすることに興味がある場合は、以下をご覧ください:

botkit.fedify.dev

BotKit by Fedify

A framework for creating your ActivityPub bots

자매 프로젝트들을 소개해 드리고자 합니다. 애플리케이션 개발을 더 쉽게 만들어주는 관련 도구들입니다:

Fedify :fedify:

Fedify(@fedify)는 ActivityPub와 다른 () 표준을 기반으로 연합 서버 애플리케이션을 구축하기 위한 라이브러리입니다. Activity Vocabulary를 위한 타입 안전한 객체, WebFinger 클라이언트·서버, HTTP Signatures 등를 제공하여 반복적인 코드를 줄이고 애플리케이션 로직에 집중할 수 있게 해줍니다.

Hollo :hollo:

Hollo(@hollo)는 Fedify로 구동되는 1인 사용자용 마이크로블로깅 서버입니다. 1인 사용자를 위해 설계되었지만, ActivityPub를 통해 완전히 연합되어 연합우주 전체의 사용자들과 상호작용할 수 있습니다. Hollo는 Mastodon 호환 API를 구현하여 자체 웹 인터페이스 없이도 대부분의 Mastodon 클라이언트와 호환됩니다.

Hollo는 또한 정식 출시 전에 최신 Fedify 기능을 테스트하는 실험장으로도 활용되고 있습니다.

BotKit :botkit:

BotKit(@botkit)은 저희의 가장 새로운 구성원으로, ActivityPub 봇을 만들기 위해 특별히 설계된 프레임워크입니다. 전통적인 Mastodon 봇과 달리, BotKit은 플랫폼별 제한(글자 수 제한 등)에 구애받지 않는 독립적인 ActivityPub 서버를 만듭니다.

BotKit의 API는 의도적으로 단순하게 설계되어 단일 TypeScript 파일로 완전한 봇을 만들 수 있습니다!


세 프로젝트 모두 @fedify-dev GitHub 조직에서 오픈 소스로 공개되어 있습니다. 각기 다른 목적을 가지고 있지만, ActivityPub 개발을 더 접근하기 쉽게 만들고 연합우주 생태계를 확장한다는 공통된 목표를 공유합니다.

이러한 프로젝트를 사용해보거나 개발에 기여하는 데 관심이 있으시다면, 다음을 확인해보세요:

botkit.fedify.dev

BotKit by Fedify

A framework for creating your ActivityPub bots

@nyeong@hackers.pub · Reply to An Nyeong (安寧)

리브랜딩을 거친 것도 학습에 치명적인듯... 예를 들어 언어 설정 파일이 bsconfig.json에서 rescript.json으로 이름이 바뀌었는데, rescript.json이 이미 있음에도 bsconfig.json을 자꾸 만들려고 한다. 일일이 사전 지시를 넣어주어야 하는데 🤔

@nyeong@hackers.pub

대-AI 시대가 열렸으면 내가 낯선 언어라도 린터, 컴파일러만 잘 되어 있으면 그걸로 피드백 줘서 PoC 하나 뚝딱할 수 있겠지? 싶어서 ReScript로 쇼기 만들어보고 있는데 아쉽게도 LLM 친구들이 ReScript를 잘 못한다

@hongminhee@hollo.social

Getting back to development today! Working on optimizing the outgoing activity queue to improve response times. Currently focusing on reducing latency when sending posts to large follower counts—should make the whole publishing experience feel much snappier.

https://github.com/fedify-dev/fedify/issues/220

github.com

Optimize activity delivery for large audiences · Issue #220 · fedify-dev/fedify

Current design Currently, when Context.sendActivity() is called, Fedify creates separate queue entries for each recipient's inbox. This design has been intentional to achieve the following benefits...

We've been working on adding custom background task support to as planned for version 1.5.0. After diving deeper into implementation, we've realized this is a more substantial undertaking than initially anticipated.

The feature would require significant API changes that would be too disruptive for a minor version update. Therefore, we've decided to postpone this feature to Fedify 2.0.0.

This allows us to:

  • Design a more robust and flexible worker architecture
  • Ensure better integration with existing task queue systems
  • Properly document the new APIs without rushing

We believe this decision will result in a more stable and well-designed feature that better serves your needs. However, some smaller improvements from our work that don't require API changes will still be included in Fedify 1.5.0 or subsequent minor updates.

We appreciate your understanding and continued support.

If you have specific use cases or requirements for background task support, please share them in our GitHub issue. Your input will help shape this feature for 2.0.0.

github.com

Support custom background tasks in worker · Issue #206 · fedify-dev/fedify

NoteWe've decided to postpone custom background task support to Fedify 2.0.0 instead of 1.5.0 as originally planned. This feature requires significant API changes that would be too disruptive for a...