Menu

PHP try catch: 예외 처리, finally, Throwable

PHP에서 실패할 수 있는 코드는 try { } 블록에 넣고, 예외가 던져지면 catch (Exception $e) { }가 실행되며 이유는 $e->getMessage()에 담깁니다. 여러 예외 타입 잡기, finally, catch (Exception)이 TypeError와 DivisionByZeroError를 놓치는 이유(Throwable을 잡으세요), 다시 던지기, 전역 핸들러를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

PHP에서는 실패할 수 있는 코드를 try { ... } 안에 넣고 실패를 catch (Exception $e) { ... }에서 처리합니다. try 블록 안의 무언가가 예외를 던지면 PHP는 일치하는 catch로 바로 넘어가고, $e->getMessage()가 무엇이 잘못되었는지 알려 줍니다. 아무것도 던져지지 않으면 catch 블록은 건너뜁니다.

divide(10, 0)을 divide(10, 5)로 바꾸고 다시 실행해 보세요. 예외가 없으므로 not printed가 출력되고 catch 블록은 건너뜁니다. try가 없으면 예외는 치명적인 "Uncaught" 에러로 스크립트를 끝냅니다. 직접 예외를 던지고 예외 클래스를 만드는 방법은 예외 페이지에 있습니다.

try catch 문법

전체 형태는 세 부분입니다. try만 필수이고, 여기에 catch가 하나 이상 있거나 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
}

catch의 타입이 무엇을 잡을지 정합니다. 그 클래스 자체와 그것을 상속하는 모든 클래스입니다. PHP 8.0부터는 변수가 필요 없을 때 생략할 수 있습니다: catch (JsonException) { ... }.

Exception, Error, Throwable 잡기

PHP 7부터 PHP 자신이 감지하는 실패(없는 메서드 호출, 잘못된 인수 타입, 0으로 나누기)는 Exception이 아니라 Error를 던지며, PHP 8은 예전의 많은 경고를 TypeError와 ValueError로 바꾸었습니다. 둘 다 Throwable 인터페이스를 구현하지만 서로 상속하지 않으므로 catch (Exception $e)는 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, ...

실행해서 각 문제를 어느 catch가 처리하는지 확인하세요.

그러면 무엇을 잡아야 할까요?

  • 무엇이 잘못될 수 있고 어떻게 대처할지 안다면 catch (SomeSpecificException $e). 대부분의 코드가 여기에 해당합니다.
  • "라이브러리나 내 코드가 일부러 던진 모든 것"에는 catch (Exception $e).
  • 가장 바깥 수준(요청 핸들러, 작업 실행기)에서 PHP 자체의 에러를 포함한 모든 실패를 기록하고 친절한 메시지를 보여 주려면 catch (Throwable $e).

여러 예외 타입 잡기

타입마다 다르게 처리하려면 catch 블록을 여러 개 쓰세요. PHP는 위에서 아래로 시도해 처음 일치하는 것을 쓰므로, 구체적인 타입이 그 부모보다 먼저 와야 합니다. 그렇지 않으면 부모가 모든 것을 먼저 잡습니다. 여러 타입을 똑같이 처리한다면 |로 catch 하나에 합치세요.

세 클래스 모두 Exception을 상속하므로 catch (Exception $e) 하나로도 잡히지만, 그러면 코드가 하나에는 404를, 나머지에는 400을 응답할 수 없게 됩니다.

finally: 항상 실행되는 코드

finally 블록은 try와 catch가 어떻게 끝났든 그 뒤에 실행됩니다. 정상 종료, 잡힌 예외, 아무도 잡지 않은 예외, return 모두입니다. try 블록이 확보한 것을 해제하는 곳입니다. 파일 닫기, 잠금 해제, 타이머 멈추기 등입니다.

good.txt에서는 finally가 함수가 실제로 반환되기 전에 실행되므로 saved가 출력되기 전에 파일이 닫힙니다. bad.txt에서는 process() 안에 catch가 없습니다. finally가 파일을 닫은 다음 예외가 호출한 쪽의 catch까지 계속 올라갑니다.

finally 안의 return은 피하세요. try 블록이 반환한 값을 대체하고, 나가던 예외를 조용히 버립니다.

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

예외 다시 던지기와 감싸기

catch 블록이 문제를 일부만 처리할 수 있을 때가 있습니다. 기록하고, 정리한 다음, 계속 올라가게 둡니다. throw $e;는 같은 예외를 다시 던집니다. 더 흔하게는 원래 원인을 유지하면서 맥락을 더하고 싶은데, 생성자의 세 번째 인수 $previous가 그 용도입니다.

호출한 쪽은 ConfigException 하나만 처리하면 되고 설정이 JSON이라는 것을 알 필요가 없으며, getPrevious()는 로그용으로 저수준 세부 사항을 유지합니다.

잡히지 않은 예외와 전역 핸들러

어떤 catch도 처리하지 않는 예외는 클래스, 메시지, 파일과 줄, 스택 트레이스를 담은 치명적 에러로 스크립트를 끝냅니다.

<?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는 first charge ok를 출력한 다음 이렇게 출력합니다.

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

운영 중인 사이트에서 이 메시지가 방문자에게 닿아서는 안 됩니다. set_exception_handler()는 아무도 잡지 않은 예외를 받는 함수를 등록합니다. 그것을 기록하고 정중한 페이지를 출력하는 단일 지점입니다.

핸들러가 실행된 다음 스크립트는 멈춥니다. throw 이후로 재개할 방법은 없습니다. PHP 자체의 에러 텍스트가 페이지에 보일지는 display_errors가 정하며, 에러 표시에서 다룹니다.

경고는 예외가 아니다

try/catch는 던져진 것만 봅니다. 예전 PHP 함수의 다수는 예외를 던지지 않습니다. false나 null을 반환하고 기껏해야 경고를 출력합니다. json_decode()가 대표적인 경우이며, 예외를 던지라고 요청하기 전까지는 try로 감싸도 아무것도 바뀌지 않습니다.

그런 플래그가 없는 함수라면 반환값을 확인하거나(if ($handle === false)), 먼저 전제 조건을 확인하세요(file_exists(), isset()). 모든 것에 하나의 규칙을 원한다면 ErrorException을 던지는 set_error_handler()가 모든 경고를 진짜 예외로 바꿉니다.

레시피: 예외로 폼 입력 검증하기

실용적인 패턴입니다. 검증 함수가 예외를 던지고, 페이지가 그것을 잡아 폼 옆에 메시지를 보여 줍니다. 실행한 다음 폼에 나이를 입력하고 Send를 누르세요. $_POST가 채워진 채로 같은 스크립트가 다시 실행됩니다.

빈 칸, twelve, 9, 30을 시도해 보세요. 실패하는 규칙마다 자신의 메시지와 함께 예외를 던지고, catch 하나가 그것을 모두 같은 빨간 줄로 바꿉니다. 폼 페이지는 이것을 바탕으로 여러 필드와 각 필드 옆의 에러를 다룹니다.

자주 묻는 질문

PHP에서 try catch는 어떻게 동작하나요?

PHP는 try { } 안의 코드를 실행합니다. 그 안의 무언가가 예외를 던지면 PHP는 try 블록의 나머지를 건너뛰고, 타입이 일치하는 첫 번째 catch 블록을 던져진 객체를 변수에 담아 실행합니다: catch (Exception $e) { echo $e->getMessage(); }. 아무것도 던져지지 않으면 catch 블록은 건너뜁니다.

PHP에서 모든 예외와 에러를 잡으려면 어떻게 하나요?

Throwable을 잡으세요: catch (Throwable $e). catch (Exception $e)는 예외만 잡고, Error를 상속하는 TypeError, ValueError, DivisionByZeroError 같은 엔진 에러는 잡지 않습니다. Exception과 Error 모두 Throwable을 구현합니다.

PHP에서 catch 블록 하나로 여러 예외를 잡으려면 어떻게 하나요?

타입을 파이프로 구분하세요: catch (InvalidArgumentException | RangeException $e). catch 블록을 연달아 여러 개 쓸 수도 있습니다. PHP는 처음 일치하는 것을 쓰므로 가장 구체적인 타입을 먼저 두세요.

try 안에 return이 있어도 finally가 실행되나요?

네. finally는 코드가 반환했든, 예외를 던졌든, 정상적으로 끝났든 try와 catch 뒤에 실행되며, try에 return이 있어도 마찬가지입니다. finally 자체가 값을 반환하면 그 값이 try의 값을 대체하므로 finally 안에서는 return을 피하세요.

PHP에서 try catch가 경고도 잡나요?

아니요. 경고와 알림(없는 배열 키 읽기 등)은 예외가 아니므로 catch는 그것을 보지 못합니다. 호출 전에 조건을 확인하거나, 함수에 예외 모드가 있다면 그것을 쓰거나(json_decode(..., flags: JSON_THROW_ON_ERROR)), set_error_handler()와 ErrorException으로 경고를 변환하세요.

Coddy 프로그래밍 언어 일러스트

Coddy로 코딩 배우기

시작하기