register_shutdown_function — catching fatal errors
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
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.");
}
});