অ্যাসিঙ্ক চেইনে এরর হ্যান্ডলিং: exceptionally, handle, এবং whenComplete

থ্রো করা এক্সেপশন কোথায় যায়?

সিঙ্ক্রোনাস কোডে কোনো এক্সেপশন কেউ catch না করলে সেটি কল স্ট্যাক ধরে উপরের দিকে ছড়িয়ে পড়ে এবং শেষ পর্যন্ত থ্রেডটিকে শেষ করে দেয়। CompletableFuture পাইপলাইনে সাধারণত সিঙ্ক্রোনাস কোডের মতো কল স্ট্যাক খুলে ধরার সুযোগ থাকে না — যে কলব্যাকটি এক্সেপশন থ্রো করেছিল সেটি পুলের কোনো ওয়ার্কার থ্রেডে চলার পর অন্য কাজে চলে যেতে পারে। বরং CompletableFuture এক্সেপশনটিকে ক্যাপচার করে এবং সেটিকে ওই স্টেজের ফলাফল হিসেবে জমা রাখে। এর পরের প্রতিটি স্টেজ বাদ পড়ে যায়, যতক্ষণ না কিছু স্পষ্টভাবে ব্যর্থতাটি হ্যান্ডেল করে।

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class PipelineExample {
    public static void main(String[] args) {
        CompletableFuture<Integer> pipeline = CompletableFuture
            .supplyAsync(() -> 10 / 0) // ArithmeticException ছোড়ে
            .thenApply(x -> x * 2);    // চলে না; ফিউচার ইতিমধ্যেই ব্যর্থ

        try {
            pipeline.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            System.out.println("Original exception: " + e.getCause());
        }
    }
}

এখানে supplyAsync স্টেজে 10 / 0 একটি ArithmeticException থ্রো করে, তাই thenApply আর চালায় না। pipeline.get() কল করলে মূল ArithmeticException-কে মোড়ানো ExecutionException পাওয়া যায়; আগের দুই পর্বে plain Future নিয়ে যে মোড়ানো আচরণ দেখেছেন, এটি সেটাই। তবে এই ব্যর্থতা সামলাতে get() পর্যন্ত অপেক্ষা করা জরুরি নয়।

exceptionally দিয়ে রিকভার করা

exceptionally একটি বিকল্প মান দিতে দেয়, যা শুধুমাত্র আগের স্টেজ ব্যর্থ হলে ব্যবহৃত হয়। আগের স্টেজ সফল হলে exceptionally-এর কলব্যাকটি সম্পূর্ণ বাদ পড়ে।

import java.util.concurrent.CompletableFuture;

public class ExceptionallyExample {
    private static int riskyDivision(int a, int b) {
        return a / b; // b শূন্য হলে ArithmeticException
    }

    public static void main(String[] args) {
        CompletableFuture<Integer> withFallback = CompletableFuture
            .supplyAsync(() -> riskyDivision(10, 0))
            .exceptionally(ex -> {
                System.err.println("Falling back after: " + ex.getMessage());
                return -1;
            });

        System.out.println(withFallback.join()); // -1
    }
}

এখানে riskyDivision(10, 0) ব্যর্থ হওয়ায় exceptionally কলব্যাক চলে এবং -1 ফিরিয়ে দেয়। এটি এমন একটি অ্যাসিঙ্ক catch ব্লকের মতো, যা সব সময় একটি মান রিটার্ন করে। তবে এটি শুধু ব্যতিক্রম দেখতে পায়, সফল ফলাফল দেখতে পায় না।

handle দিয়ে ফলাফল ও ব্যতিক্রম দুটোই দেখা

handle সব পরিস্থিতিতে কল হয় এবং আর্গুমেন্ট হিসেবে (result, ex) জোড়া পায় — এদের মধ্যে ঠিক একটি null নয়।

import java.util.concurrent.CompletableFuture;

public class HandleExample {
    private static int riskyDivision(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        CompletableFuture<Integer> handled = CompletableFuture
            .supplyAsync(() -> riskyDivision(10, 2))
            .handle((result, ex) -> {
                if (ex != null) {
                    return -1;
                }
                return result;
            });

        System.out.println(handled.join()); // 5
    }
}

এখানে handle সফল ফলাফল এবং ব্যতিক্রম উভয়ই দেখতে পারে; সফল ক্ষেত্রে ex null হওয়ায় মূল ফল 5 ফেরত আসে। handle আসলে exceptionally-এর চেয়ে বেশি শক্তিশালী, কারণ এটি সফল ফলকেও রূপান্তর করতে পারে। তবে শুধু ব্যর্থতায় বিকল্প মান দরকার হলে exceptionally কোড বেশি পরিষ্কার দেখায়।

whenComplete দিয়ে ফলাফল পরিবর্তন না করে পর্যবেক্ষণ

অনেক সময় আপনি কমপ্লিশনের প্রতিক্রিয়া জানাতে চান, কিন্তু পাইপলাইন যা তৈরি করে তা বদলাতে চান না। যেমন:

  • লগ করা
  • মেট্রিক রেকর্ড করা
  • রিসোর্স বন্ধ করা

whenComplete handle-এর মতো একই (result, exception) জোড়া পায়, কিন্তু এর ফেরত দেওয়া মান উপেক্ষিত হয়; এটি যা-ই ফেরত দিক, মূল ফলাফল বদলায় না। আগের স্টেজ ব্যর্থ হলে whenComplete থেকে পাওয়া ফিউচারটি একই ব্যতিক্রম নিয়ে ব্যর্থ থাকে, এমনকি কলব্যাকটি নিজে সঠিকভাবে চললেও।

import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;

public class WhenCompleteExample {
    private static final Logger log = Logger.getLogger(WhenCompleteExample.class.getName());

    private static int riskyDivision(int a, int b) {
        return a / b;
    }

    public static void main(String[] args) {
        CompletableFuture<Integer> observed = CompletableFuture
            .supplyAsync(() -> riskyDivision(10, 2))
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.warning("division failed: " + ex);
                } else {
                    log.info("division succeeded: " + result);
                }
            });

        System.out.println(observed.join()); // 5
    }
}

এখানে whenComplete শুধুমাত্র ফলাফলটি লগ করে; observed.join() এখনো 5-ই রিটার্ন করে। কলব্যাকের ফেরত দেওয়া মান উপেক্ষিত হয়, তাই পাইপলাইনের ফলাফল অপরিবর্তিত থাকে। ব্যর্থ স্টেজের ক্ষেত্রেও একই ব্যতিক্রম পরবর্তী ফিউচারে বহাল থাকে।

Share