Menu

try catch w PHP: wyjątki, finally i Throwable

W PHP kod, który może zawieść, trafia do bloku try { }, a catch (Exception $e) { } wykonuje się, gdy coś zostanie rzucone, z przyczyną w $e->getMessage(). Poznaj przechwytywanie kilku typów wyjątków, finally, powód, dla którego catch (Exception) nie łapie TypeError i DivisionByZeroError (łap Throwable), ponowne rzucanie i globalny handler.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

W PHP kod, który może zawieść, umieszczasz w try { ... }, a porażkę obsługujesz w catch (Exception $e) { ... }. Jeśli coś w bloku try rzuci wyjątek, PHP od razu przeskakuje do pasującego catch, a $e->getMessage() mówi, co poszło nie tak. Jeśli nic nie zostanie rzucone, blok catch jest pomijany.

Zmień divide(10, 0) na divide(10, 5) i uruchom ponownie: wyjątku nie ma, więc not printed zostaje wypisane, a blok catch jest pomijany. Bez try wyjątek zakończyłby skrypt krytycznym błędem „Uncaught”. Jak rzucać własne wyjątki i pisać klasy wyjątków, opisuje strona o wyjątkach.

Składnia try catch

Pełna forma ma trzy części. Wymagane jest tylko try oraz co najmniej jedno catch albo finally:

try {
    // code that may throw
} catch (SomeException $e) {
    // runs if SomeException (or a subclass) was thrown
} catch (OtherException | ThirdException $e) {
    // runs for either of these types
} finally {
    // always runs, thrown or not
}

Typ w catch decyduje o tym, co jest łapane: sama klasa i każda klasa, która ją rozszerza. Od PHP 8.0 zmienna jest opcjonalna, gdy jej nie potrzebujesz: catch (JsonException) { ... }.

Catch Exception vs Error vs Throwable

Od PHP 7 błędy wykrywane przez samo PHP (wywołanie brakującej metody, zły typ argumentu, dzielenie przez zero) rzucają Error, a nie Exception, a PHP 8 zamieniło wiele dawnych ostrzeżeń w TypeError i ValueError. Obie klasy implementują interfejs Throwable, ale żadna nie rozszerza drugiej, więc catch (Exception $e) nie łapie Error:

Throwable
├── Exception                 (problems a program expects and handles)
│   ├── InvalidArgumentException, RuntimeException, JsonException, ...
└── Error                     (engine errors, usually a bug in the code)
    ├── TypeError
    ├── ValueError
    ├── ArithmeticError
    │   └── DivisionByZeroError
    └── UnhandledMatchError, ...

Uruchom to, aby zobaczyć, który catch obsługuje każdy problem:

Co więc łapać?

  • catch (SomeSpecificException $e), gdy wiesz, co może pójść źle i co z tym zrobić. To większość kodu.
  • catch (Exception $e) dla „czegokolwiek, co biblioteka lub mój kod rzuciły celowo”.
  • catch (Throwable $e) na najwyższym poziomie (obsługa żądania, uruchamianie zadań), aby logować każdą porażkę, także błędy samego PHP, i pokazać przyjazny komunikat.

Przechwytywanie kilku typów wyjątków

Napisz kilka bloków catch, aby różne typy obsługiwać na różne sposoby. PHP próbuje ich od góry do dołu i używa pierwszego pasującego, więc konkretny typ musi stać przed swoim rodzicem, inaczej rodzic złapie wszystko pierwszy. Gdy kilka typów traktujesz tak samo, połącz je w jednym catch przez |:

Wszystkie trzy klasy rozszerzają Exception, więc jedno catch (Exception $e) też by je złapało, ale wtedy kod nie mógłby już odpowiedzieć 404 dla jednej i 400 dla pozostałych.

finally: kod, który zawsze się wykonuje

Blok finally wykonuje się po try i catch bez względu na to, jak się zakończyły: normalnie, ze złapanym wyjątkiem, z wyjątkiem, którego nikt nie złapał, albo przez return. To miejsce na zwolnienie tego, co zajął blok try: zamknięcie pliku, zwolnienie blokady, zatrzymanie licznika czasu.

Dla good.txt plik jest zamykany, zanim zostanie wypisane saved, bo finally wykonuje się, zanim funkcja faktycznie zwróci wartość. Dla bad.txt w process() nie ma catch: finally zamyka plik, a potem wyjątek wędruje dalej do catch wywołującego.

Unikaj return w finally: zastępuje wartość zwróconą przez blok try i po cichu wyrzuca wyjątek, który był w drodze:

function f(): string
{
    try {
        return 'from try';
    } finally {
        return 'from finally';
    }
}

function g(): string
{
    try {
        throw new RuntimeException('lost');
    } finally {
        return 'no exception reaches the caller';
    }
}

echo f(); // from finally
echo g(); // no exception reaches the caller

Ponowne rzucenie lub opakowanie wyjątku

Czasem blok catch może tylko częściowo obsłużyć problem: zalogować go, posprzątać, a potem puścić dalej. throw $e; rzuca ten sam wyjątek ponownie. Częściej chcesz dodać kontekst, zachowując pierwotną przyczynę, i do tego służy trzeci argument konstruktora, $previous:

Wywołujący ma do czynienia z jednym ConfigException i nie musi wiedzieć, że konfiguracja jest w JSON, a getPrevious() zachowuje niskopoziomowe szczegóły do logu.

Nieprzechwycone wyjątki i globalny handler

Wyjątek, którego nie obsłuży żaden catch, kończy skrypt krytycznym błędem z klasą, komunikatem, plikiem i linią oraz śladem stosu:

<?php
// No try/catch anywhere in this file
function charge(int $cents): void
{
    if ($cents <= 0) {
        throw new DomainException("Amount must be positive, got $cents");
    }
}

charge(500);
echo "first charge ok\n";
charge(-1);
echo "never printed\n";

PHP wypisuje first charge ok, a potem:

PHP Fatal error:  Uncaught DomainException: Amount must be positive, got -1 in /home/index.php:6
Stack trace:
#0 /home/index.php(12): charge()
#1 {main}
  thrown in /home/index.php on line 6

Na działającej stronie ten komunikat nigdy nie powinien trafić do odwiedzających. set_exception_handler() rejestruje funkcję, która dostaje każdy wyjątek, którego nikt nie złapał: jedno miejsce, by go zalogować i wypisać uprzejmą stronę:

Handler się wykonuje, a potem skrypt się zatrzymuje; nie da się wznowić działania po throw. To, czy na stronie w ogóle pojawia się tekst błędu PHP, kontroluje display_errors, opisane w raportowaniu błędów.

Ostrzeżenia to nie wyjątki

try/catch widzi tylko to, co zostało rzucone. Wiele starszych funkcji PHP nie rzuca wyjątków: zwracają false lub null i co najwyżej wypisują ostrzeżenie. json_decode() to typowy przypadek i otoczenie go przez try niczego nie zmienia, dopóki nie poprosisz go o rzucanie:

Dla funkcji bez takiej flagi sprawdzaj wartość zwracaną (if ($handle === false)) albo najpierw warunek wstępny (file_exists(), isset()). set_error_handler(), który rzuca ErrorException, zamienia każde ostrzeżenie w prawdziwy wyjątek, jeśli chcesz jednej zasady dla wszystkiego.

Przepis: walidacja formularza przez wyjątki

Praktyczny wzorzec: funkcje walidujące rzucają wyjątki, a strona je łapie i pokazuje komunikat obok formularza. Uruchom, wpisz wiek w formularzu i naciśnij Send: ten sam skrypt uruchomi się ponownie z wypełnionym $_POST.

Spróbuj pustego pola, twelve, 9 i 30. Każda niespełniona zasada rzuca wyjątek z własnym komunikatem, a jedno catch zamienia je wszystkie w tę samą czerwoną linię. Strona o formularzach rozwija to o kilka pól i błędy przy każdym z nich.

Najczęściej zadawane pytania

Jak działa try catch w PHP?

PHP wykonuje kod w try { }. Jeśli coś w nim rzuci wyjątek, PHP pomija resztę bloku try i wykonuje pierwszy blok catch o pasującym typie, z rzuconym obiektem w zmiennej: catch (Exception $e) { echo $e->getMessage(); }. Jeśli nic nie zostanie rzucone, bloki catch są pomijane.

Jak przechwycić wszystkie wyjątki i błędy w PHP?

Przechwyć Throwable: catch (Throwable $e). catch (Exception $e) łapie tylko wyjątki, a nie błędy silnika, takie jak TypeError, ValueError czy DivisionByZeroError, które rozszerzają Error. Zarówno Exception, jak i Error implementują Throwable.

Jak przechwycić kilka wyjątków w jednym bloku catch w PHP?

Oddziel typy kreską pionową: catch (InvalidArgumentException | RangeException $e). Możesz też napisać kilka bloków catch jeden po drugim; PHP używa pierwszego pasującego, więc najbardziej szczegółowe typy umieszczaj na początku.

Czy finally wykona się, gdy w try jest return?

Tak. finally wykonuje się po try i catch bez względu na to, czy kod zwrócił wartość, rzucił wyjątek, czy zakończył się normalnie, także gdy try zawiera return. Jeśli finally samo zwraca wartość, zastępuje ona wartość z try, więc unikaj return w finally.

Czy try catch łapie ostrzeżenia w PHP?

Nie. Ostrzeżenia i notatki (na przykład odczyt brakującego klucza tablicy) nie są wyjątkami, więc catch nigdy ich nie widzi. Sprawdź warunek przed wywołaniem, użyj trybu wyjątków funkcji, jeśli go ma (json_decode(..., flags: JSON_THROW_ON_ERROR)), albo zamieniaj ostrzeżenia przez set_error_handler() i ErrorException.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ