Skip to content
Muhammet Şafak
tr
Languages 5 min read

Exception and Error Handling in PHP

A practical guide to PHP exceptions and error handling: try-catch-finally blocks, multiple catch clauses, custom exception classes, and global handlers.


PHP error handling runs through two distinct mechanisms: errors and exceptions. Understanding the difference between them is not just a technical detail — where you use each one directly affects how predictable your application behaves.

An error is a condition triggered by the PHP engine itself: an undefined variable, a division by zero, a call to a non-existent function. An exception, on the other hand, is an object that you or a library you are using deliberately throws, carries up the call stack, and can be caught. Starting with PHP 7, these two worlds largely unified under the Throwable interface — the Error class is now catchable too. But that unification does not change the mental model: catching errors is mostly about building a safety net; exceptions are part of the business flow.

try-catch-finally

The basic structure is similar across languages; in PHP it looks like this:

function bolme(int $bolunen, int $bolen): float
{
    if ($bolen === 0) {
        throw new InvalidArgumentException("Sıfıra bölme.");
    }
    return $bolunen / $bolen;
}

try {
    echo bolme(10, 2) . "\n"; // 5
    echo bolme(10, 0) . "\n"; // exception fırlatılır
} catch (InvalidArgumentException $e) {
    echo "Geçersiz argüman: " . $e->getMessage() . "\n";
} finally {
    echo "Her durumda çalışır.\n";
}

Output:

5
Geçersiz argüman: Sıfıra bölme.
Her durumda çalışır.

A few points worth emphasizing.

I used InvalidArgumentException — not Exception. PHP’s SPL library offers a meaningful hierarchy: RuntimeException, LogicException, InvalidArgumentException, OutOfRangeException, and so on. Throwing a plain Exception gives the catch side no way to filter; you are essentially saying “something happened, not sure what.” Choosing the right exception type lets callers handle the error in a meaningful way.

The finally block runs regardless of whether an exception was caught. Typical uses: closing a database connection, releasing a lock, deleting a temporary file. Using return or a new throw inside finally is possible, but at that point the code becomes hard to follow — tread carefully.

Multiple catch blocks

You can write multiple catch blocks for different exception types on a single try block:

try {
    $sonuc = veritabanindenCek($id);
} catch (NotFoundException $e) {
    return null; // kayıt yok, normal durum
} catch (DatabaseException $e) {
    logger()->error($e->getMessage());
    throw new ServiceException("Veri alınamadı.", previous: $e);
}

The approach here is deliberate: NotFoundException silently returns null because “record not found” is not an error — it is an expected outcome. DatabaseException, however, is re-thrown one layer up as a more general exception, with the original exception preserved via the previous parameter. This is the standard way to add abstraction without losing the stack trace.

Since PHP 8.0, you can also combine multiple exception types in a single catch block:

catch (NotFoundException | InvalidArgumentException $e) {
    // handle both types the same way
}

Custom exception classes

Writing your own exception classes enriches the error vocabulary of your application. A minimal example:

class PaymentFailedException extends RuntimeException
{
    public function __construct(
        private readonly string $transactionId,
        string $message = '',
        ?\Throwable $previous = null
    ) {
        parent::__construct($message, 0, $previous);
    }

    public function getTransactionId(): string
    {
        return $this->transactionId;
    }
}

Code that catches this class can call $e->getTransactionId(). You have concrete context you can feed directly into log records, alerting systems, or the response returned to the user.

This is especially valuable when writing libraries. If all exceptions produced by your library extend a single base exception, consumers can catch them all with one catch block whenever they need to.

Global exception handler

For uncaught exceptions, you can register a global handler using set_exception_handler():

set_exception_handler(function (\Throwable $e): void {
    logger()->critical($e->getMessage(), [
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => $e->getTraceAsString(),
    ]);
    http_response_code(500);
    echo json_encode(['error' => 'Beklenmeyen bir hata oluştu.']);
    exit(1);
});

Frameworks like Laravel and Symfony already manage this through their own Handler mechanisms. If you are working with bare PHP or writing a small library, this handler is critical.

Error levels and error_reporting

PHP’s native error system is a separate dimension. The basic rule: in development, keep all error levels visible; in production, route errors to a log and never display them to the user. Configuring this through php.ini is preferred, but in shared hosting environments where that access is unavailable, error_reporting() and ini_set() can step in at runtime:

// Geliştirme ortamı: her şeyi göster
error_reporting(E_ALL);
ini_set('display_errors', '1');

// Production: hataları gösterme, logla
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php-errors.log');

PHP error levels use a bitmap structure:

ConstantValueDescription
E_ERROR1Fatal runtime error; execution stops.
E_WARNING2Non-fatal runtime warning.
E_PARSE4Parse error; generated by the parser.
E_NOTICE8Notice of a possible issue; e.g. undefined variable.
E_STRICT2048Forward-compatibility suggestions.
E_ALL32767All errors and warnings.

You can combine and exclude levels with bitwise operators:

error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);

The @ operator: why to avoid it

In PHP, the @ operator suppresses all errors generated by the expression it precedes. It works technically; in practice it is a significant anti-pattern.

// Don't do this
echo @$tanimsizDegisken;

// Do this instead
echo $tanimsizDegisken ?? '';

The @ operator hides a problem — it does not solve it. A real error slips through silently, and you end up spending hours debugging. Since PHP 8.0, @ can even be disabled entirely. Null coalescing (??), isset(), or proper exception handling is always the cleaner choice.


PHP 7 and later matured exception handling considerably. The Throwable unification, the use of previous with named arguments, union catch blocks — these are all part of that progress. But the core idea has not changed: an exception represents a situation that is not “unexpected” but unpredictable. What you can foresee, you handle with if/else. What you cannot, you wrap in an exception hierarchy and communicate meaningfully. Adopting this as a mental model clarifies exactly what each catch block should capture and what it should do with it.

Tags: #PHP
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind