CodeGym /課程 /JAVA 25 SELF /非同步程式碼中的錯誤處理:exceptionally、handle

非同步程式碼中的錯誤處理:exceptionally、handle

JAVA 25 SELF
等級 55 , 課堂 3
開放

1. 問題:非同步程式碼中的例外

在一般(同步)程式碼中很簡單:如果方法中發生例外,它會沿呼叫堆疊「拋」上去,我們可以用 try-catch 來捕捉。例如:

try {
    int x = 1 / 0;
} catch (ArithmeticException ex) {
    System.out.println("除以零!");
}

在非同步程式碼中情況更複雜。當我們透過 CompletableFuture.supplyAsync 啟動任務時,它會在另一個執行緒中執行。如果那裡發生例外,它不會拋回主執行緒!相反地,它會被「封裝」在 CompletableFuture 物件中,之後如果你呼叫 get()join(),就會以 ExecutionException 的形式收到該例外。

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // 糟了,這裡出錯了!
    return 1 / 0;
});

try {
    Integer result = future.get(); // 這裡會拋出例外!
} catch (Exception ex) {
    System.out.println("發生錯誤: " + ex.getMessage());
}

但如果你不呼叫 get()(順帶一提,這本身就不太「非同步」),而是透過 thenApply 等方法來串接,錯誤可能會「悄悄消失」。因此,在非同步程式設計中,務必要在 CompletableFuture 的鏈中捕捉並處理錯誤。

2. exceptionally 方法:處理錯誤並回傳值

exceptionally 方法允許你在鏈的前一個或多個階段出現例外時加以捕捉、處理,並回傳替代值。這就像 catch,但用於非同步的資料流。

簽名:

CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

使用範例

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    System.out.println("正在執行危險的計算...");
    if (Math.random() > 0.5) {
        throw new RuntimeException("出現問題了!");
    }
    return 42;
});

future = future.exceptionally(ex -> {
    System.out.println("發生錯誤: " + ex.getMessage());
    return 0; // 回傳「安全」值
});

thenAccept 範例

future.thenAccept(result -> System.out.println("結果: " + result));

輸出(可能的結果):

正在執行危險的計算...
發生錯誤: 出現問題了!
結果: 0
正在執行危險的計算...
結果: 42

重要! exceptionally 只有在它之前的鏈條中出現未處理的例外時才會觸發。若一切順利,它會直接將結果傳遞到下一步。

3. handle 方法:同時處理結果與錯誤的通用處理器

有時我們需要同時處理結果錯誤。例如,一切正常——就回傳結果;如果出錯——回傳備用方案或記錄錯誤。

簽名:

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
  • 第一個參數 — 結果(若發生錯誤則為 null),
  • 第二個 — 例外(若一切正常則為 null)。

使用範例

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("隨機錯誤!");
    return 100;
});

CompletableFuture<Integer> safeFuture = future.handle((result, ex) -> {
    if (ex != null) {
        System.out.println("偵測到錯誤: " + ex.getMessage());
        return -1;
    }
    return result;
});

safeFuture.thenAccept(r -> System.out.println("最終結果: " + r));

輸出:

偵測到錯誤: 隨機錯誤!
最終結果: -1
最終結果: 100

當你希望不論任務是成功或失敗都要採取動作時,適合使用 handle。它是永遠會被呼叫的通用結果處理器,並接收兩個參數:結果(若一切正常)與例外(若發生錯誤)。

此方法非常適合集中記錄錯誤、在不終止鏈的情況下回傳預設值,或是優雅地結束非同步情境。

範例:

CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> 10 / 0) // 這裡會發生錯誤
    .handle((result, ex) -> {
        if (ex != null) {
            System.out.println("錯誤: " + ex.getMessage());
            return 0; // 預設值
        }
        return result;
    });

System.out.println(future.join()); // 會輸出 0

不同於 exceptionally 僅在發生錯誤時才會反應,handle總是觸發,讓你在同一處理器內處理兩種結果,並維持整條鏈的順暢。

4. whenComplete 方法:完成後的副作用動作

有時我們不需要改變結果,只想在任務完成後執行某個動作——例如記錄任務已完成,無論是成功或失敗。

簽名:

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
  • 第一個參數 — 結果(若發生錯誤則為 null),
  • 第二個 — 例外(若成功則為 null)。

使用範例

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("錯誤!");
    return 10;
});

future.whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("執行時發生錯誤: " + ex.getMessage());
    } else {
        System.out.println("成功完成,結果: " + result);
    }
});

重要差異:
whenComplete 不會改變結果或例外,只是執行動作。如果在 whenComplete 中發生新的例外,它會被「附加」到既有的例外上。

範例:只記錄,不干預

future
    .whenComplete((res, ex) -> {
        System.out.println("任務已完成。有錯誤嗎? " + (ex != null));
    })
    .thenAccept(r -> System.out.println("提供給使用者的結果: " + r));

5. 特性與實作細節

Best practices:如何正確在 CompletableFuture 中處理錯誤

  • 務必在非同步任務鏈中加入錯誤處理(exceptionallyhandlewhenComplete)。否則,錯誤可能不被注意,應用程式會出現不可預期的行為。
  • 不要在主執行緒中,在沒有 try-catch 的情況下呼叫 get()join()——這會把非同步程式碼變回同步,且可能導致阻塞。
  • 若需要在錯誤時回傳「備用」值——使用 exceptionallyhandle
  • 若是為了副作用(記錄日誌、通知使用者)——使用 whenComplete
  • 在鏈中可以組合:例如先用 exceptionally 處理錯誤,再用 whenComplete 記錄,接著繼續處理結果。
  • 請記得,若錯誤未被處理,它會「流向」下一個呼叫 get()/join() 的地方,並可能導致應用程式當機。

方法順序

  • 如果你使用 exceptionally,它只會攔截在它之前出現的錯誤。
  • 如果在 exceptionally 之後鏈中再次發生錯誤(例如在 thenApply 中),需要另外處理。
  • handle 是通用的——無論是否發生錯誤都會觸發。

方法的組合

CompletableFuture.supplyAsync(() -> {
    // ...
})
.handle((result, ex) -> {
    if (ex != null) return "錯誤: " + ex.getMessage();
    return result;
})
.whenComplete((res, ex) -> {
    System.out.println("任務已結束,結果: " + res);
});

如果沒有處理錯誤會怎樣?

如果例外未被處理而你呼叫了 get()join(),它會以 ExecutionException(或 CompletionException)的形式被拋出,應用程式可能因此失敗。

6. 在 CompletableFuture 中處理錯誤的常見錯誤

錯誤 №1:未加入任何錯誤處理。 如果既沒有 exceptionally、也沒有 handlewhenComplete,錯誤會在到達下一次 get()/join() 呼叫前「悄然消失」,而該呼叫點可能離發生處很遠。

錯誤 №2:在主執行緒中未搭配 try-catch 就使用 get()/join()。 這會讓非同步程式碼變成同步,並可能造成阻塞或意外當機。

錯誤 №3:誤解處理器觸發的位置。 exceptionally 只會捕捉它之前的錯誤。如果在它之後再次發生錯誤,它不會處理這個錯誤。

錯誤 №4:有處理錯誤,但沒有回傳值。exceptionallyhandle 中務必回傳值,否則下一個階段會收到 null(或根本收不到任何東西)。

錯誤 №5:混淆 handle 與 whenComplete。 handle 可以改變結果,而 whenComplete 只能執行動作(例如記錄)。如果你想改變結果——請使用 handle

錯誤 №6:重複撰寫錯誤處理邏輯。 通常可以把錯誤處理集中在一處,避免重複程式碼——例如使用集中式的 handle 或共用處理器。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION