0

set_exception_handler — global exception handler

Intermediate5 min read·php-10-007
laravel-src

Concept

set_exception_handler($callback) registers a global handler for uncaught exceptions. When an exception propagates all the way to the top of the call stack without being caught, PHP calls this handler instead of displaying a fatal error. It's the last line of defense before the application terminates.

Handler signature: handler(\Throwable $e): void. The script terminates after the handler returns — you cannot prevent termination from here.

Use cases: Generate a user-friendly error page, log the exception with full context before termination, send an alert to Sentry/Bugsnag/Rollbar, return a JSON error response for API requests.

Laravel's exception handler: App\Exceptions\Handler implements \Illuminate\Foundation\Exceptions\Handler. It overrides render() to transform exceptions into HTTP responses, report() to log/notify, and uses $dontReport / $dontFlash to exclude sensitive data. Registered via App\Http\Kernel.

restore_exception_handler(): Restores the previous handler (handlers are stacked like error handlers).

Difference from try/catch at application entry: set_exception_handler catches exceptions that slip past all try/catch blocks in the application. It's for truly unexpected exceptions. Expected exceptions (business logic errors) should be caught explicitly.

Code Example

php
<?php
declare(strict_types=1);

// Global exception handler for a simple PHP application
set_exception_handler(function (\Throwable $e): void {
    // Log the full exception
    error_log(sprintf(
        "Uncaught %s: %s in %s:%d\nStack trace:\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    // User-facing response
    if (PHP_SAPI === 'cli') {
        fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
        exit(1);
    }

    $isApi = str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/api/');
    if ($isApi) {
        header('Content-Type: application/json', true, 500);
        echo json_encode(['error' => 'Internal Server Error']);
    } else {
        http_response_code(500);
        include __DIR__ . '/views/errors/500.html';
    }
});

// With Sentry integration
set_exception_handler(function (\Throwable $e): void {
    \Sentry\captureException($e);

    http_response_code(500);
    echo json_encode(['error' => 'An unexpected error occurred']);
});

// Laravel's handler (simplified internal structure)
class Handler
{
    protected array $dontReport = [
        \Illuminate\Auth\AuthenticationException::class,
        \Illuminate\Validation\ValidationException::class,
    ];

    public function report(\Throwable $e): void
    {
        if ($this->shouldntReport($e)) return;
        $this->logger->error($e->getMessage(), ['exception' => $e]);
    }

    public function render($request, \Throwable $e): \Symfony\Component\HttpFoundation\Response
    {
        if ($e instanceof \Illuminate\Validation\ValidationException) {
            return response()->json($e->errors(), 422);
        }
        if ($e instanceof \Illuminate\Auth\AuthenticationException) {
            return response()->json(['message' => 'Unauthenticated'], 401);
        }
        return response()->json(['message' => 'Server Error'], 500);
    }
}