0

register_shutdown_function — catching fatal errors

Advanced5 min read·php-10-008
laravel-src

Concept

register_shutdown_function($callback) registers a function that PHP calls when the script finishes executing — whether it exits normally, via exit(), via an uncaught exception, or (critically) via a fatal error. This is the only mechanism for catching fatal errors in PHP.

Fatal errors and shutdown functions: When PHP encounters a fatal error (E_ERROR, E_PARSE, stack overflow, memory exhaustion), the regular set_error_handler and set_exception_handler are NOT called. But registered shutdown functions ARE. Inside the shutdown function, error_get_last() returns an array with the last error — including fatal errors.

What error_get_last() returns: ['type' => E_ERROR, 'message' => '...', 'file' => '...', 'line' => ...] or null if no error occurred. The type will be E_ERROR for fatal errors.

Limitations: By the time the shutdown function runs on a fatal error, the output buffer may be active and the HTTP response code may not have been sent. You can clean the buffer with ob_end_clean() and send a proper error response. Memory is constrained — if the fatal was an OOM error, you may not have much memory to work with.

Multiple shutdown functions: Each call to register_shutdown_function adds to the list; all registered functions run in FIFO order. They're run AFTER object destructors.

Laravel uses this: Laravel's error handler (\Illuminate\Foundation\Bootstrap\HandleExceptions) registers a shutdown function to catch fatals and route them through the same exception handling pipeline, turning them into FatalThrowableError instances.

Code Example

php
<?php
declare(strict_types=1);

// Detect and log fatal errors
register_shutdown_function(function(): void {
    $error = error_get_last();

    if ($error === null) return; // no error, clean shutdown

    $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];
    if (!in_array($error['type'], $fatalTypes, strict: true)) return;

    // Log the fatal error
    error_log(sprintf(
        "FATAL ERROR [%d]: %s in %s:%d",
        $error['type'],
        $error['message'],
        $error['file'],
        $error['line']
    ));

    // Clean any buffered output
    while (ob_get_level() > 0) {
        ob_end_clean();
    }

    // Send error response
    if (!headers_sent()) {
        http_response_code(500);
        header('Content-Type: application/json');
    }
    echo json_encode(['error' => 'A fatal error occurred']);
});

// Multiple shutdown functions — run in order
register_shutdown_function(function(): void {
    echo "Cleanup 1: close connections\n";
});
register_shutdown_function(function(): void {
    echo "Cleanup 2: flush logs\n";
});
// Both run when script terminates

// Using shutdown function for cleanup (like defer in Go)
function withCleanup(callable $work, callable $cleanup): mixed
{
    register_shutdown_function($cleanup);
    return $work();
}

// Simulating OOM detection
register_shutdown_function(function(): void {
    $error = error_get_last();
    if ($error && str_contains($error['message'], 'memory')) {
        // OOM situation — emergency logging only (limited memory available)
        error_log("CRITICAL: Out of memory. Increase memory_limit.");
    }
});