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.