Propagacja wyjątków: kiedy należy wychwytywać wyjątki?

44

Metoda A wywołuje metodę B, która z kolei wywołuje metodę C.

Nie ma obsługi wyjątków w metodzie B lub metodzie C. Ale istnieje metoda obsługi wyjątków w metodzie A.

W metodzie C występuje wyjątek.

Teraz wyjątek dotyczy metody MethodA, która odpowiednio ją obsługuje.

Co jest z tym nie tak?

Moim zdaniem, w pewnym momencie program wywołujący wykona metodę B lub metodę C, a gdy w tych metodach wystąpią wyjątki, to co zyska się na obsłudze wyjątków w tych metodach, co zasadniczo jest tylko blokiem try / catch / wreszcie zamiast po prostu pozwolić oni bańki do callee?

Oświadczenie lub konsensus dotyczący obsługi wyjątków ma zostać wygenerowany, gdy wykonanie nie może być kontynuowane z tego właśnie powodu - wyjątku. Rozumiem. Ale dlaczego nie złapać wyjątku w dalszej części łańcucha zamiast blokowania try / catch do samego końca.

Rozumiem to, kiedy musisz zwolnić zasoby. To zupełnie inna sprawa.

Daniel Frost
źródło
46
Jak myślisz, dlaczego konsensus polega na przejściu łańcucha połowów?
Caleth
Dzięki dobremu IDE i właściwemu stylowi kodowania możesz wiedzieć, że można wywołać wyjątek, gdy wywoływana jest metoda. Poradzenie sobie lub umożliwienie propagacji jest decyzją osoby dzwoniącej. Nie widzę z tym żadnego problemu.
Hieu Le
14
Jeśli metoda nie jest w stanie obsłużyć wyjątku i po prostu go przeprojektowuje, powiedziałbym, że to zapach kodu. Jeśli metoda nie jest w stanie obsłużyć wyjątku i nie musi robić nic innego po zgłoszeniu wyjątku, wówczas try-catchblok nie jest wcale potrzebny .
Greg Burghardt
7
„Co jest z tym nie tak?” : nic
Ewan
5
Łapanie tranzytowe (które nie obejmuje wyjątków różnych typów itp.) Pokonuje cały cel wyjątków. Zgłaszanie wyjątków jest złożonym mechanizmem i zostało zbudowane celowo. Jeśli przypadkowe przypadki użycia byłyby zamierzonymi przypadkami użycia, wystarczyłoby zaimplementować Result<T>typ (typ, który przechowuje wynik obliczeń lub błąd) i zwrócić go z innych funkcji rzucania. Propagowanie błędu na stosie wiązałoby się z odczytaniem każdej zwracanej wartości, sprawdzaniem, czy jest to błąd i zwracaniem błędu, jeśli tak jest.
Alexander

Odpowiedzi:

139

Zgodnie z ogólną zasadą nie wychwytuj wyjątków, chyba że wiesz, co z nimi zrobić. Jeśli MethodC zgłasza wyjątek, ale MethodB nie ma użytecznego sposobu na jego obsłużenie, wówczas powinien umożliwić wyjątek rozprzestrzenienie się do MethodA.

Jedynymi powodami, dla których metoda powinna mieć mechanizm catch and rethrow, są:

  • Chcesz przekonwertować jeden wyjątek na inny, który ma większe znaczenie dla dzwoniącego powyżej.
  • Chcesz dodać dodatkowe informacje do wyjątku.
  • Potrzebujesz klauzuli catch, aby oczyścić zasoby, które przeciekłyby bez niej.

W przeciwnym razie wychwytywanie wyjątków na niewłaściwym poziomie powoduje, że kod po cichu zawodzi bez dostarczania użytecznych informacji zwrotnych do kodu wywołującego (i ostatecznie użytkownika oprogramowania). Alternatywa złapania wyjątku, a następnie natychmiastowego jego ponownego wprowadzenia jest bezcelowa.

Simon B.
źródło
28
@GregBurghardt, jeśli twój język ma coś podobnego try ... finally ..., użyj tego, nie
łap
19
„wyłapanie wyjątku, a następnie natychmiastowe jego ponowne wprowadzenie nie ma sensu” W zależności od języka i tego, jak sobie z tym poradzisz, może to być aktywnie szkodliwe dla bazy kodu. Często ludzie próbujący to usunąć, usuwają wiele informacji o wyjątku, takich jak oryginalny stacktrace. Mam do czynienia z kodem, w którym osoba dzwoniąca otrzymuje wyjątek, który całkowicie wprowadza w błąd co do tego, co się stało i gdzie.
JimmyJames
7
„nie wychwytuj wyjątków, chyba że wiesz, co z nimi zrobić”. Na pierwszy rzut oka to brzmi rozsądnie, ale później powoduje problemy. To, co tutaj robisz, to wyciekanie szczegółów implementacji do twoich rozmówców. Wyobraź sobie, że używasz określonego ORM w swojej implementacji do ładowania danych. Jeśli nie wychwycisz wyjątków tego ORM, ale po prostu pozwolisz im się rozwinąć, nie możesz zastąpić warstwy danych bez naruszenia kompatybilności z istniejącymi użytkownikami. To jeden z bardziej oczywistych przypadków, ale może stać się dość podstępny i trudno go złapać.
Voo
11
@Voo W przykładzie ty nie wiesz co z nim zrobić. Zawiń go w udokumentowanym wyjątku specyficznym dla Twojego kodu, np. LoadDataExceptionI dołącz oryginalne szczegóły wyjątku zgodnie z funkcjami języka, aby przyszli opiekunowie mogli zobaczyć główną przyczynę bez konieczności dołączania debuggera i wymyślania, jak odtworzyć problem.
Colin Young,
14
@ Voo Wygląda na to, że przegapiłeś powód „Chcesz przekonwertować jeden wyjątek na inny, który jest bardziej znaczący dla powyższego dzwoniącego”.
jpmc26
21

Co jest z tym nie tak?

Absolutnie niczego.

Teraz wyjątek dotyczy metody MethodA, która odpowiednio ją obsługuje.

„odpowiednio sobie z tym radzi” jest ważną częścią. To sedno obsługi strukturalnego wyjątku.

Jeśli Twój kod może zrobić coś „użytecznego” z wyjątkiem, skorzystaj z niego. Jeśli nie, to daj spokój.

. . . dlaczego nie złapać wyjątku w dalszej części łańcucha zamiast blokowania try / catch do końca.

Właśnie to powinieneś robić. Jeśli czytasz kod, który ma handler / rethrowers „do samego końca”, to [prawdopodobnie] czytasz jakiś dość kiepski kod.

Niestety, niektórzy programiści widzą bloki catch jako kod „bojlera”, który wrzucają (bez zamierzonej gry słów) do każdej pisanej metody, często dlatego, że tak naprawdę nie „dostają” obsługi wyjątków i uważają, że muszą coś dodać wyjątki nie „uciekają” i nie zabijają ich programów.

Częściową trudnością jest to, że przez większość czasu problem ten nawet nie zostanie zauważony, ponieważ wyjątki nie są zgłaszane przez cały czas, ale kiedy już się pojawią , program będzie marnował dużo czasu i staraj się stopniowo usuwać stos wywołań, aby dostać się do miejsca, które faktycznie robi coś przydatnego w przypadku wyjątku.

Phill W.
źródło
7
Co gorsza, aplikacja przechwytuje wyjątek, a następnie rejestruje go (gdzie, mam nadzieję, nie będzie tam wiecznie) i próbuje kontynuować jak zwykle, nawet jeśli naprawdę nie może.
Solomon Ucko
1
@SolomonUcko: Cóż, to zależy. Jeśli na przykład piszesz prosty serwer RPC, a nieobsługiwany wyjątek bąbelki aż do głównej pętli zdarzeń, jedyną rozsądną opcją jest zarejestrowanie go, wysłanie błędu RPC do zdalnego elementu równorzędnego i wznowienie przetwarzania zdarzeń. Inną alternatywą jest zabicie całego programu, który doprowadzi twoje SRE do szału, gdy serwer zginie w produkcji.
Kevin
@Kevin W takim przypadku powinien istnieć jeden catchna najwyższym poziomie, który rejestruje błąd i zwraca odpowiedź błędu. catchWszędzie nie posypano bloków. Jeśli nie masz ochoty wymieniać każdego możliwego sprawdzonego wyjątku (w językach takich jak Java), po prostu zawiń go RuntimeExceptionzamiast tam logować, próbując kontynuować i napotkać więcej błędów, a nawet luk.
Solomon Ucko
8

Musisz zrobić różnicę między bibliotekami a aplikacjami.

Biblioteki mogą swobodnie zgłaszać niewyłapane wyjątki

Projektując bibliotekę, w pewnym momencie musisz pomyśleć o tym, co może pójść nie tak. Parametry mogą znajdować się w niewłaściwym zakresie lub nullzasoby zewnętrzne mogą być niedostępne itp.

Twoja biblioteka najczęściej nie będzie miała sposobu, aby sobie z nimi poradzić w rozsądny sposób. Jedynym sensownym rozwiązaniem jest zgłoszenie odpowiedniego wyjątku i pozwolenie deweloperowi aplikacji zająć się tym.

Aplikacje powinny zawsze w pewnym momencie wychwytywać wyjątki

Kiedy wychwycony jest wyjątek, lubię je kategoryzować jako Błędy lub Błędy krytyczne . Zwykły błąd oznacza, że ​​pojedyncza operacja w mojej aplikacji nie powiodła się. Na przykład nie można zapisać otwartego dokumentu, ponieważ nie można było zapisać miejsca docelowego. Jedyną rozsądną rzeczą, jaką może zrobić Aplikacja, jest poinformowanie użytkownika, że ​​operacja nie może zostać pomyślnie zakończona, podanie czytelnych dla człowieka informacji dotyczących problemu, a następnie pozwolenie użytkownikowi zdecydować, co dalej.

Fatal Error jest błąd główny logika aplikacji nie można odzyskać. Na przykład, jeśli sterownik urządzenia graficznego ulegnie awarii w grze wideo, Aplikacja nie będzie mogła „z wdziękiem” poinformować użytkownika. W takim przypadku należy zapisać plik dziennika i, jeśli to możliwe, użytkownik powinien zostać w taki czy inny sposób poinformowany.

Nawet w tak poważnym przypadku Aplikacja powinna traktować ten wyjątek w znaczący sposób. Może to obejmować zapisanie pliku dziennika, wysłanie raportu o awarii itp. Nie ma powodu, aby aplikacja nie reagowała w jakiś sposób na wyjątek.

MechMK1
źródło
Rzeczywiście, jeśli masz bibliotekę do czegoś takiego jak operacje zapisu na dysku lub, powiedzmy, inne manipulacje sprzętowe, możesz skończyć na różnego rodzaju nieoczekiwanych zdarzeniach. Co się stanie, jeśli dysk twardy zostanie wyciągnięty podczas pisania? Co zwalnia napęd CD podczas czytania? To jest poza kontrolą i podczas mógłby coś zrobić (np udawać, że to był udany), to często dobra praktyka, aby rzucić wyjątek dla użytkownika biblioteki i pozwolić im zdecydować. Być HDDPluggedOutDuringWritingExceptionmoże można sobie z tym poradzić i nie jest to fatalne dla aplikacji. Program może zdecydować, co z tym zrobić.
VLAZ
1
@VLAZ To, co jest śmiertelne vs. nieśmiertelne, zależy od aplikacji. Biblioteka powinna powiedzieć, co się stało. Aplikacja musi zdecydować, jak zareagować.
MechMK1
0

Co jest nie tak z opisanym wzorcem, że metoda A nie będzie w stanie odróżnić trzech scenariuszy:

  1. Metoda B zawiodła w oczekiwany sposób.

  2. Metoda C zawiodła w sposób nieprzewidziany w metodzie B, ale podczas gdy metoda B wykonywała operację, którą można bezpiecznie porzucić.

  3. Metoda C zawiodła w sposób nieprzewidziany w metodzie B, ale podczas gdy metoda B przeprowadzała operację, która ustawiła rzeczy w rzekomo tymczasowy niespójny stan, którego B nie udało się wyczyścić z powodu awarii C.

Jedynym sposobem, w jaki metoda A będzie w stanie rozróżnić te scenariusze, będzie sytuacja, gdy wyjątek zgłoszony z B będzie zawierał informacje wystarczające do tego celu lub jeśli rozwinięcie stosu dla metody B spowoduje pozostawienie obiektu w jawnie unieważnionym stanie. Niestety większość ram wyjątków sprawia, że ​​oba wzorce są niewygodne, co zmusza programistów do podejmowania decyzji projektowych „mniejszego zła”.

supercat
źródło
2
Scenariusz 2 i 3 to błędy w metodzie B. Metoda A nie powinna próbować ich naprawiać.
Sjoerd
@ Sjoerd: W jaki sposób metoda B ma przewidzieć wszystkie przyczyny niepowodzenia metody C?
supercat
Przez dobrze znane wzorce, takie jak wykonywanie wszystkich operacji, które mogą wrzucać zmienne tymczasowe, a następnie z operacjami, które nie mogą rzucać - np. Zamiana - zamień stare na nowy stan. Innym wzorcem jest definiowanie operacji, które można bezpiecznie powtórzyć, dzięki czemu można ponowić operację bez obawy, że się zepsują. Są pełne książki na temat pisania „wyjątkowego kodu bezpiecznego”, więc nie mogę wam tutaj wszystkiego powiedzieć.
Sjoerd
Jest to dobra rzecz, aby w ogóle nie używać wyjątków (co byłoby doskonałą decyzją, imho). Wydaje mi się jednak, że tak naprawdę nie odpowiada na pytanie, ponieważ PO wydaje się przede wszystkim stosować wyjątki, a jedynie pyta, gdzie powinien być haczyk.
cmaster
@Sjoerd Metoda B staje się znacznie łatwiejsza do uzasadnienia, czy język zabrania wyjątków. Ponieważ w takim przypadku faktycznie widzisz wszystkie ścieżki przepływu sterowania przez B i nie musisz zgadywać, którzy operatorzy mogą być przeciążeni w sposób rzucający (C ++), aby uniknąć scenariusza 3. Płacimy dużo za kod jasność i bezpieczeństwo, aby być leniwym w zwracaniu błędów przez „zwykłe” zgłaszanie wyjątków. Ponieważ ostatecznie obsługa błędów jest istotną częścią kodu.
cmaster