นากกายต้นฉบับรับรางสีเขียวและรางสีแดงมารวมเป็นซองผลลัพธ์ชนิดเดียวที่จุด boundary

Java CompletableFuture.handle: รวมทางสำเร็จและล้มเหลวไว้ที่ boundary เดียว

พี่โชว์Engineering

async pipeline มักต้องเปลี่ยนทั้งค่าปกติและ exception ให้เป็น response type เดียวก่อนออกจาก service CompletableFuture.handle เรียก BiFunction ไม่ว่า stage ก่อนหน้าจะสำเร็จหรือล้มเหลว โดยฝั่งที่ไม่เกิดจะเป็น null แล้วคืน future ของค่าที่แปลงแล้วครับ

import java.util.concurrent.CompletableFuture;

public class Main {
    record Outcome<T>(T value, String error) {
        static <T> Outcome<T> ok(T value) { return new Outcome<>(value, null); }
        static <T> Outcome<T> failed(String error) { return new Outcome<>(null, error); }
    }

    static CompletableFuture<Outcome<Integer>> parseAsync(String raw) {
        return CompletableFuture
                .supplyAsync(() -> Integer.parseInt(raw))
                .handle((value, error) -> error == null
                        ? Outcome.ok(value)
                        : Outcome.failed("invalid integer"));
    }

    public static void main(String[] args) {
        assert parseAsync("42").join().equals(Outcome.ok(42));
        assert parseAsync("bad").join().equals(Outcome.failed("invalid integer"));
        System.out.println("handle tests passed");
    }
}

ต่างจากเพื่อนบ้านอย่างไร

exceptionally ทำงานเฉพาะทางล้มเหลวและต้องคืน fallback ชนิดเดียวกับผลปกติ whenComplete เห็นทั้ง value/error แต่มีไว้ทำ side effect และส่ง outcome เดิมต่อ ส่วน handle เปลี่ยนทั้งสองทางเป็นชนิดใหม่ได้ จึงเหมาะกับ boundary ที่ต้องทำ error envelope หรือสรุป telemetry แล้วจบ chain

ต้องระวังว่า exception มักถูกห่อใน CompletionException อย่าเผย error.getMessage() ตรง ๆ แก่ผู้ใช้ เพราะอาจมีข้อมูลระบบภายในและชนิด wrapper อาจทำให้การจัดหมวดผิด แยก public error code ออกจาก diagnostic log และ unwrap เฉพาะเท่าที่นโยบายกำหนด

หาก callback ใน handle โยน exception ใหม่ future ผลลัพธ์ก็ล้มเหลวอีกครั้ง ดังนั้น mapping ที่ boundary ควรเล็ก deterministic และไม่ทำ I/O เพิ่ม หากต้อง retry ให้สร้างนโยบาย retry ก่อนมาถึง handle; การคืน fallback ในทุก exception โดยไม่แยก timeout, cancellation และ validation อาจทำให้ incident ถูกซ่อน

Recap

  • handle เห็นทั้ง value และ error และแปลงชนิดผลลัพธ์ได้
  • ใช้ whenComplete สำหรับ observation และ exceptionally สำหรับ fallback ฝั่ง error
  • อย่าเผย raw exception message ข้าม trust boundary
  • แยก cancellation/timeout จาก error ธุรกิจแทนการกลืนรวมครับ