register_shutdown_function
Registers a callback that PHP runs when the script finishes or exit() is called.
register_shutdown_function(callable $callback, mixed ...$args): void- Added in PHP 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$callback | callable | The callback to run at shutdown. It runs as part of the request, so it can still produce output and access output buffers. |
$argsoptional | mixed | Zero or more values passed to the callback when it runs. Variadic; evaluated at registration time. |
Return value
voidNo value is returned.
Errors
Throws a TypeError if callback is not a valid callable. Exceptions thrown inside a shutdown callback cannot be caught by the main script and surface as uncaught fatal errors.
Changelog
- PHP 8.4A parameterless exit() inside a registered shutdown function resets the exit code to 0. Calling exit() with an explicit status still overwrites the previous exit code, as in all versions.
- PHP 8.0An invalid callback now throws a TypeError at registration time instead of raising a warning.
A registered shutdown callback runs after the script body ends — on normal completion, on exit(), and after a fatal error. That last case is what makes it valuable: it is the only place you can react to an E_ERROR such as a memory-limit or a call to an undefined method, because a fatal error skips finally blocks and exception handlers entirely.
The two standard jobs
Fatal-error reporting: register a callback, inspect error_get_last(), and log or render a friendly page if the last error was fatal. And cleanup: delete temporary files, release locks, flush a metrics buffer, close a long-running connection. Callbacks run in registration order, and a shutdown callback may register further ones, which are appended to the queue.
Limits worth knowing
Shutdown callbacks are skipped entirely if the process is killed with SIGTERM or SIGKILL, so this is not a durable cleanup mechanism. They are exempt from max_execution_time, which means slow shutdown code can hang a request past its timeout. Under some web servers the working directory changes before they run, so use absolute paths. And arguments are captured when you register, not when the callback fires.
Example
When to use it
- Logging fatal errors by inspecting error_get_last() after a crash
- Deleting temporary files or releasing a lock however the script ends
- Flushing a buffered metrics or profiling payload at the end of every request
- Rendering a friendly error page when the memory limit is exhausted mid-render
Frequently asked questions
Does register_shutdown_function run after a fatal error?
Yes, and that is its main use — it runs after E_ERROR, unlike finally blocks or exception handlers. Inspect error_get_last() inside the callback to see what happened.
In what order do shutdown functions run?
In the order they were registered. A shutdown callback may register more, and those are appended to the end of the queue, but calling exit() in any of them cancels everything still pending.
Will shutdown functions run if the process is killed?
No. SIGTERM and SIGKILL terminate the process without running the queue, so do not rely on this for cleanup that must survive a deploy restart or an out-of-memory kill by the OS.
More examples
Catching a fatal error
Prints 'working' then 'clean exit'. Replace the last line with something that triggers a real fatal error and the same callback prints the captured message instead — this is the only hook that still runs after an E_ERROR.
<?php
register_shutdown_function(function (): void {
$err = error_get_last();
if ($err !== null && ($err['type'] & (E_ERROR | E_PARSE | E_COMPILE_ERROR)) !== 0) {
echo "fatal captured: {$err['message']}\n";
} else {
echo "clean exit\n";
}
});
echo "working\n";Guaranteed cleanup with captured arguments
Prints 'scratch data', then at shutdown 'removed demo.tmp' and 'finished in under a second: true'. Both arguments were captured by value at registration time, so reassigning $start afterwards would not change what the callback sees.
<?php
$start = hrtime(true);
file_put_contents('demo.tmp', 'scratch data');
register_shutdown_function(function (string $file, int $started): void {
if (is_file($file)) {
unlink($file);
echo "removed $file\n";
}
echo 'finished in under a second: ', var_export((hrtime(true) - $started) < 1_000_000_000, true), "\n";
}, 'demo.tmp', $start);
echo file_get_contents('demo.tmp'), "\n";exit() halts the remaining callbacks
Prints 'body' then 'A runs' and stops. B is skipped because exit() inside a shutdown callback terminates the queue, and on PHP 8.4+ that parameterless exit also resets the process exit status from 3 to 0.
<?php
register_shutdown_function(function (): void {
echo "A runs\n";
exit;
});
register_shutdown_function(function (): void {
echo "B never runs\n";
});
echo "body\n";
exit(3);
Discussion