Wyjątki – kiedy, jak i po co?

Kiedy używać wyjątków a kiedy zwracać kod błędu? Okazuje się, że sprawa nie jest całkowicie jasna. Czy każda funkcja powinna rzucać wyjątkiem?

Wyjątki są fajnym mechanizmem do obsługi błędów. Niestety można ich nadużywać co często prowadzi do gorszego kodu.

Ogólna zasada mówi:

Wyjątki służą do obsługi sytuacji wyjątkowych (exceptional). Nie należy ich używać do sterowania przepływem programu (flow control).

Bardzo zły przykład

Spójrzmy na taki kawałek kodu. Program służy do wyszukiwania w obiekcie lub jego potomkach:

public function search($data) {
   if ($this->data == $data) {
      throw new ResultException('Found in '.$this->name)
   }
   foreach ($this->getChildren() as $child) {
      $child->search($data);
   }
}

I teraz użycie:

try {
   $object->search("some value");
} catch (ResultException $e) {
   echo $e->getMessage();
}

Używamy wyjątku do wykrycia zupełnie normalnej sytuacji – znalezienia poszukiwanej wartości w drzewie. Bez problemu można ten kod napisać zwracając znalezioną wartość. Wyjątki tylko zaciemniają sprawę.

Wniosek – nie używaj wyjątków do kontroli przepływu programu.

Kiepski przykład

Próbujemy zapisać dane z formularza do bazy. Wyjątek służy do wykrycia sytuacji niepoprawnej:

try {
   $data = $form->getData();
   $entity = new Entity($data);
   $manager->save($entity);
} catch (EntityValidationError $e) {
   echo 'Data was invalid because: '.$e->getMessage();
}

Tu też mamy problem. Niby złe dane są błędem użytkownika ale czy faktycznie potrzebujemy wyjątku? Dane można parsować, walidować oraz zwrócić tabelę błędów. Jeśli użytkownik błędnie wypełni dwa pola – mamy problem. Nie można rzucić dwóch różnych wyjątków naraz.

Wniosek – nawet w sytuacjach błędów wyjątki nie zawsze się sprawdzają.

Do czego tak naprawdę służą wyjątki?

Dochodzimy do sedna sprawy. Wyjątek oznacza, że część aplikacji uległa awarii. Czy to ze względu na błąd użytkownika, czy też przez błędne warunki. Jeśli nie obsłużymy wyjątku aplikacja się wysypuje. Dlatego można przyjąć takie zasady:

  • Wyjątek oznacza wysypanie się aplikacji lub jej znacznej części.
  • Rzucanie wyjątku powinno wystąpić gdy przyczyna błędu jest zewnętrzna i aplikacja (lub część) nie może kontynuować pracy.
  • Nie używajmy wyjątków do zachowania, które nie jest wyjątkową sytuacją w programie.

Wyjątki to takie micro-crashe w programie. Oznaczają, że problem jest krytyczny. Obsługa (łapanie) wyjątków powinno nastąpić gdy jesteśmy w stanie coś zrobić z krytycznym problemem.

Dobry przykład

Mamy kilka serwisów, które składają się na program do pobierania i analizy danych z WWW. Nasz kod może wyglądać tak:

// każdy z serwisów rzuca wyjątkami w razie niepowodzenia
try {
    // serwis pobierający dane z sieci
   $webCrawler = $this->get('web_crawler_service');
   $data = $webCrawler->download('http://some.site/');

   // serwis parsujący pobrane dane
   $domParser = $this->get('dom_parser_service');
   $parsedValue = $domParser->parse($data);

   // serwis analizujący przydatność danych
   $dataAnalyser = $this->get('data_analyser_service');
   $valueToSave = $dataAnalyser->analyse($parsedValue);

   // zapisywanie danych
   $database = $this->get('database_service');
   $database->persist($valueToSave);
} catch ($e) {
    // jeśli coś się nie powiodło logujemy przyczynę problemu
    $logger = $this->get('logger_service');
    $logger->error('Error while fetching data. Reason: '.$e->getMessage());
}

Ten przykład jest w miarę dobrym użyciem wyjątków. Oczywiście każdy z serwisów mógłby zwracać false czy inną wartość oznaczającą błąd. Jednak wtedy skończylibyśmy z drabinką if-ów. Interesuje nas jednak pozytywna ścieżka wykonania programu. Wszelkie błędy trzeba po prostu zalogować w jeden sposób.

To prowadzi do wniosków:

  • Wyjątki są wyrzucane przez niezależne części aplikacji. Serwisy można tu traktować jako podaplikacje. Gdy nie umią poradzić sobie z danymi wyrzucają wyjątek. Obsługa leży w wyższej części systemu.
  • Pozytywna ścieżka (tzw. happy path) działania programu nie korzysta z wyjątków.
  • Sytuacja błędu wymaga obsługi przez użytkownika (który przeczyta logi).
  • Wyjątki dotyczą zewnętrznych problemów – złego adresu URL, błędnej konfiguracji parsera, błędnej analizy, braku potrzebnego serwisu itp.

Co dalej?

W internecie łatwo znajdziemy instrukcje jak używać wyjątków. Bardzo trudno za to znaleźć odpowiedź na pytanie kiedy i po co.

Warto przeczytać artykuł zniechęcający do używania wyjątków. Jest też dyskusja na stackoverwlow w tym temacie. Możemy też zapoznać się z tą dyskusją o wyjątkach.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *