CodeGym /Kursy /JAVA 25 SELF /Łańcuchowanie wyjątków (Exception Chaining)

Łańcuchowanie wyjątków (Exception Chaining)

JAVA 25 SELF
Poziom 24 , Lekcja 2
Dostępny

1. Wprowadzenie

W tym wykładzie omówimy ważną technikę pracy z wyjątkami — łańcuchowanie wyjątków (exception chaining). Ta technika pozwala nie tracić informacji o pierwotnej przyczynie błędu, nawet jeśli „opakowujesz” jeden wyjątek drugim.

W realnych aplikacjach często bywa tak, że błąd powstaje głęboko w stosie wywołań — na przykład podczas pracy z bazą danych, systemem plików lub siecią. Załóżmy, że masz metodę, która łączy się z bazą danych i może rzucić SQLException. Jednak na poziomie logiki biznesowej nie chcesz „zaśmiecać” kodu technicznymi detalami i wolisz rzucać własny wyjątek, na przykład UserManagementException.

Co się stanie, jeśli po prostu rzucić nowy wyjątek?

try {
    // coś z bazą danych
} catch (SQLException e) {
    throw new UserManagementException("Błąd podczas pracy z użytkownikami");
}

Problem:
W takim przypadku informacja o tym, co dokładnie wydarzyło się w bazie danych (i cały stos wywołań!), zostaje utracona. W logu zobaczysz tylko UserManagementException, a przyczyna pozostanie nieznana.

2. Rozwiązanie: opakowanie wyjątku źródłowego (chaining)

Java pozwala „opakować” jeden wyjątek drugim, przekazując wyjątek źródłowy jako przyczynę (cause) do konstruktora nowego wyjątku. To właśnie nazywa się łańcuchowaniem wyjątków.

Jak to zrobić?

Większość standardowych i własnych wyjątków ma konstruktor przyjmujący drugi parametr — Throwable cause:

public UserManagementException(String message, Throwable cause) {
    super(message, cause);
}

Użycie:

try {
    // coś z bazą danych
} catch (SQLException e) {
    throw new UserManagementException("Błąd podczas pracy z użytkownikami", e);
}

Teraz, jeśli spojrzysz na stos wywołań (printStackTrace()), zobaczysz zarówno swój wyjątek, jak i cały łańcuch aż do pierwotnej przyczyny!

3. Jak pobrać przyczynę wyjątku

Każdy obiekt typu Throwable ma metodę getCause(), która zwraca wyjątek źródłowy (lub null, jeśli go nie ma).

Przykład:

try {
    // ...
} catch (UserManagementException e) {
    Throwable cause = e.getCause();
    if (cause != null) {
        System.out.println("Pierwotna przyczyna: " + cause);
    }
    e.printStackTrace();
}

Po co to?

  • Do debugowania: widzisz nie tylko „co poszło nie tak” na najwyższym poziomie, lecz także gdzie dokładnie wystąpił błąd głębiej w stosie.
  • Do logowania: można zapisać w logu cały łańcuch błędów.
  • Do przekazywania informacji między warstwami aplikacji: warstwa biznesowa może „opakować” techniczny wyjątek we własny, nie tracąc szczegółów.

4. Przykład: łańcuchowanie wyjątków w prawdziwej aplikacji

Załóżmy, że masz metodę, która ładuje użytkownika z bazy danych:

public User loadUser(String username) throws UserManagementException {
    try {
        // Kod, który może rzucić SQLException
        // ...
    } catch (SQLException e) {
        throw new UserManagementException("Nie udało się załadować użytkownika: " + username, e);
    }
}

Gdzie UserManagementException — to twój własny wyjątek:

public class UserManagementException extends Exception {
    public UserManagementException(String message, Throwable cause) {
        super(message, cause);
    }
}

Co stanie się w przypadku błędu?

  • W logach będzie widać zarówno twój wyjątek, jak i oryginalny SQLException ze wszystkimi szczegółami.
  • W razie potrzeby można uzyskać dostęp do pierwotnej przyczyny przez getCause().

5. Jak wygląda stos wywołań przy łańcuchowaniu wyjątków

Przykładowy output:

UserManagementException: Nie udało się załadować użytkownika: user
    at UserService.loadUser(UserService.java:15)
    ...
Caused by: java.sql.SQLException: Connection refused
    at ...

Widać tu wszystko: pełny łańcuch wywołań, gdzie powstał błąd biznesowy i jaki techniczny wyjątek był przyczyną.

6. Praktyka: implementujemy łańcuchowanie wyjątków

Krok 1. Tworzymy własny wyjątek:

public class UserManagementException extends Exception {
    public UserManagementException(String message) {
        super(message);
    }
    public UserManagementException(String message, Throwable cause) {
        super(message, cause);
    }
}

Krok 2. Używamy łańcuchowania:

try {
    // coś ryzykownego
} catch (SQLException e) {
    throw new UserManagementException("Błąd podczas pracy z bazą danych", e);
}

Krok 3. Obsługa na najwyższym poziomie:
Na najwyższym poziomie programu łapiemy nasz własny wyjątek i wypisujemy komunikat o błędzie wraz z całym łańcuchem przyczyn.

public class Main {
    public static void main(String[] args) {
        try {
            runUserManagement();
        } catch (UserManagementException e) {
            System.err.println("Wystąpił błąd: " + e.getMessage());
            // Wypisujemy łańcuch przyczyn
            Throwable cause = e.getCause();
            while (cause != null) {
                System.err.println("Przyczyna: " + cause.getMessage());
                cause = cause.getCause();
            }
        }
    }

    private static void runUserManagement() throws UserManagementException {
        try {
            // symulacja błędu bazy danych
            throw new SQLException("Brak połączenia z bazą danych");
        } catch (SQLException e) {
            throw new UserManagementException("Błąd podczas pracy z bazą danych", e);
        }
    }
}

7. Typowe błędy przy pracy z łańcuchowaniem wyjątków

Błąd nr 1: rzucanie nowego wyjątku bez cause.

catch (SQLException e) {
    throw new UserManagementException("Błąd", /* brak cause! */);
}

Źle: traci się informację o pierwotnej przyczynie.

Błąd nr 2: nie zaimplementowano konstruktora z cause we własnym wyjątku.
Jeśli w twojej klasie wyjątku nie ma konstruktora z Throwable cause, nie przekażesz przyczyny — trzeba go dodać ręcznie.

Błąd nr 3: łapiesz i tłumisz wyjątek, nie przekazując go dalej.

catch (SQLException e) {
    // Tylko logujemy i milczymy
}

Źle: błąd „znika”, a program działa niepoprawnie.

try {
    userService.loadUser("user");
} catch (UserManagementException e) {
    System.err.println("Błąd: " + e.getMessage());
    if (e.getCause() != null) {
        System.err.println("Pierwotna przyczyna: " + e.getCause());
    }
    e.printStackTrace();
}
1
Zadanie
JAVA 25 SELF, poziom 24, lekcja 2
Niedostępne
Detektyw danych: Ustalenie pierwotnej przyczyny awarii
Detektyw danych: Ustalenie pierwotnej przyczyny awarii
1
Zadanie
JAVA 25 SELF, poziom 24, lekcja 2
Niedostępne
Ścieżka do błędu: Łańcuch awarii w systemie raportowania
Ścieżka do błędu: Łańcuch awarii w systemie raportowania
1
Zadanie
JAVA 25 SELF, poziom 24, lekcja 2
Niedostępne
"Zewnętrzny błąd" z "przyczyną": Analiza przyczyn
"Zewnętrzny błąd" z "przyczyną": Analiza przyczyn
1
Zadanie
JAVA 25 SELF, poziom 24, lekcja 2
Niedostępne
Katastrofa kosmiczna: Wielopoziomowy łańcuch awarii
Katastrofa kosmiczna: Wielopoziomowy łańcuch awarii
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION