Pokemon exception handling :)

Mechanizm wyjątków w Javie jest czasem dość irytujący. Część wyjątków wymaga obowiązkowego obsłużenia. Nie jest to przyjemne, szczególnie dla początkujących deweloperów. W efekcie dochodzi do “zjadania” wyjątków, co w żargonie zwane jest “pokemon exception handling” (złap je wszystkie! – gotta catch them all!). Warto porozmawiać o tym “pokemonowym problemie”, który często jest zmorą w niskiej jakości kodzie.

Obsługiwalność wyjątków

Zacznijmy od tego, że wyjątki w Javie dzielą się na dwie grupy:

  • checked – ich obsłużenie wymusza kompilator; to wszystkie wyjątki, które są subklasą Throwable, poza RuntimeException i Error
  • unchecked – wszystkie subklasy RuntimeException lub Error, tych nie trzeba obsługiwać

Wyjątki “checked” oznaczają sytuację błędu, z którego system powinien wyjść obronną reką. Dla przykładu są to np. błędy sieciowe, czy błędy dostępu do plików. Tego typu sytuacja spowoduje przerwanie operacji, ale nie powinna być śmiertelna dla całej aplikacji. Dlatego też, ich obsługa jest wymuszona.

Wyjątki “unchecked” to sytuację, które nigdy nie powinny wystąpić i zwykle oznaczają krytyczny błąd (np. OutOfMemoryError), lub jednoznaczny błąd programistyczny lub testerski (np. sławny NPE). Takie wyjątki są zawsze poważnym problemem i powinny być załatwiane na etapie dewelopmentu aplikacji. Ich obsługa jest możliwa, ale niezalecana.

Więc gdzie jest ten problem z wyjątkami w Javie?

Wyjątki typu “checked” mogą być poważnie irytujące. Weźmy pod uwagę taką sytuację:

File niceFile = new File("nice.file");
niceFile.createNewFile();

Jak widać na powyższym przykładzie, chcemy stworzyć nowy plik. I wszystko jest OK, dopóki nie spojrzymy na wynik kompilacji…

Unhandled exception: java.io.IOException

No rzeczywiście nie jest to miłe. Kompilator w bardzo nieprzyjemny sposób upomina nas o obowiązku obsłużenia wyjątku. W tym momencie nasz IDE może nam pomóc autogenerując kod obsługujący:

File myNiceFile = new File("nice.file");
try {
    myNiceFile.createNewFile();
} catch (IOException e) {}

No i załatwione! Kompilator jest zadowolony i wszystko działa. Niestety zrobiliśmy w tym kroku jeden z najgorszych błędów jaki istnieje – zjedliśmy wyjątek.

Co się dzieje jeśli używamy pokemonowej obsługi wyjątków?

Najgorszy w tej sytuacji jest fakt ukrycia wystąpienia wyjątku. Wszystko działa do czasu, aż w końcu wyjątek wystąpi. Mamy wtedy idealne ukrycie błędu i aplikacja stara się działać dalej bez żadnego alarmu. Taki kod potrafi bez problemu przebić testy i znaleźć się na produkcji, gdzie szkody mogą być ogromne.

Taki styl działania aplikacji jest przeciwieństwem filozofii fail-fast, która zakłada jak najszybsze przerwanie operacji przy wykrytym błędzie.

Pokemonowa obsługa błędów jest klastycznym objawem niskiej jakości kodu, występuje zwykle w tandemie z innymi problemami. Jeśli programista nie ma świadomości o problemie spowodowanym takim kodem, to z pewnością popełni też inne tragiczne błędy.

Co w takim razie należy zrobić, by nie zjadać wyjątków?

Żeby uniknąć najgorszych problemów wystarczy nie ukrywać błędu. W ten sposób neutralizujemy najgorsze szkody. Nawet najprostsze wypisanie błędu będzie o niebo lepsze:

try {
    niceFile.createNewFile();
} catch (IOException e) {
    e.printStackTrace();
}

Nie jest to pod żadnym pozorem rozwiązanie najlepsze… Jeśli operacja nie może kontynuować po wystąpieniu tego błędu, to na miejscu jest zamiana tego wyjątku na błąd unchecked:

try {
    niceFile.createNewFile();
} catch (IOException e) {
    throw new RuntimeException(e);
}

Taka implementacja jest już lepsza, ale może iść o krok dalej:

try {
    niceFile.createNewFile();
} catch (IOException e) {
    throw new NiceFileCreationException(e);
}

W takim wypadku tworzymy wyjątek specyficzny dla sytuacji, co pozwala nam go obsłużyć dokładnie w taki sposób, jaki wymagany jest przez logikę aplikacji. Zawracam tutaj uwagę na zastosowanie exception chaining. Łańcuchowe wyjątki to też bardzo dobra praktyka, bo wtedy wyjątek specyficzny ma sobie informacje o oryginalnym wyjątku.

Ostatnim sposobem na poprawną obsługę, jest stworzenie procedury alternatywnej:

try {
    niceFile.createNewFile();
} catch (IOException e) {
    handleNiceFileCreationException(e);
}

W procedurze alternatywnej możemy mieć zaimplementowane sposoby radzenia sobie z sytuacją.

Zjadanie wyjątków jest prawdziwą klasyką…

Widziałem w swojej karierze setki wystąpień takiej tragedii. Jest to standardowa rzecz w wielu systemach, które jakością swojego kodu wołają o pomstę do nieba.

W 2014 byliśmy świadkami kompletnej kompromitacji PKW, której system obsługujący wybory był kompletnie niewydolny. Po wycieku kodu systemu naszym oczom okazał się system, w którym był popełniony każdy błąd jaki dało się popełnić. Obsługa pokemonowa była tam na każdym kroku i z pewnością odpowiadała za dużą część problemów z systemem…

Leave A Comment