Ein nächtlicher Import liest eine CSV-Datei, die an diesem Tag fehlt. Um den Aufruf steht ein try-Block, und trotzdem greift kein catch. PHP schreibt eine Warnung ins Protokoll, $inhalt ist false, und das Skript importiert munter null Datensätze. Mit PHP set_error_handler lässt sich genau das ändern: Die Funktion leitet Warnungen, Hinweise und Deprecations an eigenen Code weiter, der sie zum Beispiel in eine ErrorException verwandelt. Danach fängt der catch-Block, was er fangen soll.
Der Handler ist die Weiche zwischen beiden Welten. Was er nicht selbst erledigt, geht weiter an die Standardbehandlung von PHP.
Warum try/catch keine Warnung fängt
PHP kennt zwei getrennte Fehlersysteme. Das ältere arbeitet mit Meldungen, die eine Fehlerstufe tragen: E_WARNING, E_NOTICE, E_DEPRECATED und so weiter. Das neuere arbeitet mit Ausnahmen. Ein catch reagiert nur auf Ausnahmen, an einer Warnung läuft er vorbei:
<?php
try {
$inhalt = file_get_contents('daten.csv');
} catch (Exception $e) {
echo 'Wird nie erreicht.';
}
var_dump($inhalt); /* bool(false) */
/* Warning: file_get_contents(daten.csv):
Failed to open stream: No such file
or directory */
Manche Erweiterungen werfen inzwischen selbst, mysqli zum Beispiel seit PHP 8.1, nachzulesen im Tutorial zu mysqli_errno() und Ausnahmen. Viele Dateifunktionen melden sich dagegen weiterhin nur mit einer Warnung. Für sie schlägt PHP set_error_handler die Brücke zwischen beiden Welten.
Die Signatur von PHP set_error_handler()
<?php
set_error_handler(
?callable $callback,
int $error_levels = E_ALL
): ?callable
restore_error_handler(): true
/* Signature of the callback */
handler(
int $errno,
string $errstr,
string $errfile,
int $errline
): bool
/*
* $callback Called for every error that
* matches $error_levels.
* null restores PHP's default.
* $error_levels Bitmask of error types passed
* to the handler (default E_ALL).
* Return value The previous handler or null.
* Callback returns false: PHP's standard
* error handler runs as well.
*/
Der Rückgabewert von PHP set_error_handler ist der Handler, der vorher aktiv war, beim ersten Aufruf also null. Wichtiger für den Alltag ist der Rückgabewert der Rückruffunktion selbst, denn er entscheidet, ob PHP die Meldung zusätzlich ausgibt. Darum geht es im nächsten Abschnitt.
Der Error Handler und sein Rückgabewert
Die Rückruffunktion bekommt vier Werte: die Fehlerstufe als Zahl, den Meldungstext, die Datei und die Zeile. Ein fünfter Parameter $errcontext ist mit PHP 8.0 entfallen. Alte Beispiele, die ihn als Pflichtparameter erwarten, enden heute mit ArgumentCountError: Too few arguments, sobald die erste Warnung auftritt. Ein minimaler Handler für PHP set_error_handler gibt die Werte einfach aus:
<?php
set_error_handler(function (
int $errno,
string $errstr,
string $errfile,
int $errline
): bool {
echo "[$errno] $errstr in Zeile $errline\n";
return true;
});
echo $nichtDa;
/* [2] Undefined variable $nichtDa in Zeile 12 */
Der Rückgabewert des Handlers ist bei PHP set_error_handler der heikelste Punkt. Nur false lässt die Standardbehandlung weiterlaufen: Die Warnung erscheint zusätzlich, und error_get_last() ist gefüllt. Jeder andere Wert gilt als erledigt. Gemessen gilt das für true, für 0 und auch für einen Handler ganz ohne return. Wer das return vergisst und im Handler selbst nichts ausgibt, verschluckt also still jede Meldung. Dazu kommt: Ein mit PHP set_error_handler registrierter Handler läuft auch dann, wenn error_reporting auf 0 steht. Die Prüfung der Maske ist Aufgabe des Handlers. Den Weg einer Meldung durch diese Weichen zeigt das Diagramm.
flowchart TD
A[Meldung ausgelöst] --> B{Fatal oder Parse?}
B -- ja --> C[kein Handler]
C --> D[Shutdown-Funktion]
B -- nein --> E{in error_levels?}
E -- nein --> F[Standardbehandlung]
E -- ja --> G[Handler aufgerufen]
G --> H{Rückgabe false?}
H -- ja --> F
H -- nein --> I[erledigt]
G -. wirft ErrorException .-> J[try/catch]
Warnungen in eine ErrorException umwandeln
Der Handler prüft zuerst, ob die Meldung laut aktueller Maske überhaupt gemeldet werden soll, und wirft sonst eine ErrorException. Diese Klasse hat genau die Felder, die der Handler mitbringt: Meldung, Code, Schweregrad, Datei und Zeile. Der try-Block aus der Einleitung funktioniert damit ohne jede Änderung, nur der catch nennt jetzt die passende Klasse.
<?php
set_error_handler(function (
int $errno,
string $errstr,
string $errfile,
int $errline
): bool {
if (!(error_reporting() & $errno)) {
return false;
}
throw new ErrorException(
$errstr, 0, $errno, $errfile, $errline
);
});
try {
$inhalt = file_get_contents('daten.csv');
} catch (ErrorException $e) {
echo $e->getSeverity(); /* 2 = E_WARNING */
echo $e->getMessage();
}
var_dump(isset($inhalt)); /* bool(false) */
Die letzte Zeile zeigt den eigentlichen Nutzen. Die Zuweisung an $inhalt findet gar nicht statt, der Code läuft also nicht mit false weiter, als wäre nichts gewesen. Die Fehlerstufe steht in getSeverity(), getCode() bleibt 0. Fängt niemand die Ausnahme, endet das Skript mit Fatal error: Uncaught ErrorException, und das ist bei einem Import, der sonst leere Daten schreiben würde, das bessere Ende.
Der @-Operator und error_reporting() im Handler
Die Prüfzeile mit dem bitweisen Und hat einen zweiten Zweck: Sie respektiert den Unterdrückungsoperator @. Vor PHP 8 lieferte error_reporting() innerhalb des Handlers bei einem unterdrückten Aufruf 0, daher stammt die Prüfung auf === 0 in vielen alten Beispielen. Seit PHP 8 kommt dort 4437 heraus, die Summe der Stufen, die sich nicht unterdrücken lassen. Ein Vergleich mit 0 schlägt also nie mehr an.
<?php
error_reporting(E_ALL);
set_error_handler(function (int $no, string $s): bool {
echo error_reporting(), "\n";
return true;
});
file_get_contents('fehlt.txt'); /* Maske */
@file_get_contents('fehlt.txt'); /* 4437 */
Ohne @ erscheint die eingestellte Maske, hier der Wert von E_ALL, der sich je nach Konfiguration und PHP-Version unterscheidet. Die 4437 ist dagegen fest. Mit der Zeile if (!(error_reporting() & $errno)) { return false; } liefert @file_get_contents() auf eine fehlende Datei weiterhin still false, ohne Ausnahme. So bleibt jedes bewusst gesetzte @ wirksam, auch mit eigenem Handler.
restore_error_handler(): den vorigen Handler zurückholen
Handler liegen auf einem Stapel. Jeder Aufruf von PHP set_error_handler legt einen neuen obendrauf, restore_error_handler() nimmt den obersten wieder herunter. Ein Versuch mit zwei Handlern A und B zeigt es: Eine Warnung landet erst bei B, nach einem restore_error_handler() bei A und nach einem zweiten bei der Standardbehandlung. set_error_handler(null) stellt die Standardbehandlung ebenfalls her.
Der Rückgabewert von restore_error_handler() ist immer true, auch bei acht Aufrufen auf einem leeren Stapel. Als Kontrolle taugt er nicht, überzählige Aufrufe fallen nicht auf. Umso wichtiger ist, dass Setzen und Zurücknehmen fest zusammengehören. Das sicherste Muster dafür ist finally, denn es läuft auch dann, wenn der Code dazwischen eine Ausnahme wirft. Ohne finally bliebe der temporäre Handler genau in diesem Fall aktiv.
<?php
$hinweise = [];
$sammler = function (int $no, string $s)
use (&$hinweise): bool {
$hinweise[] = $s;
return true;
};
set_error_handler(
$sammler,
E_DEPRECATED | E_USER_DEPRECATED
);
try {
$export = altbibliothek_export($daten);
} finally {
restore_error_handler();
}
Die Altbibliothek erzeugt Deprecations, die im eigenen Protokoll nur stören. Sie landen gesammelt in $hinweise, und nach dem Block gilt wieder der Handler, den der vorige Aufruf von PHP set_error_handler gesetzt hatte. Wirft altbibliothek_export() eine Ausnahme, wird trotzdem zurückgesetzt.
Nur bestimmte Fehlerarten abfangen
Der zweite Parameter $error_levels ist ein Filter. Mit E_NOTICE | E_USER_NOTICE geht eine E_WARNING gemessen an der Funktion vorbei direkt in die Standardbehandlung. Für verschiedene Fehlerarten lässt sich aber kein zweiter Handler parallel registrieren. Ein neuer Aufruf von PHP set_error_handler ersetzt den aktiven, er ergänzt ihn nicht. Wer unterschiedliche Fehlerarten unterschiedlich behandeln will, verzweigt deshalb in einem einzigen Handler, am elegantesten mit match:
<?php
set_error_handler(function (
int $errno,
string $errstr,
string $errfile,
int $errline
): bool {
if (!(error_reporting() & $errno)) {
return false;
}
return match ($errno) {
E_DEPRECATED,
E_USER_DEPRECATED => error_log("Alt: $errstr"),
E_WARNING,
E_USER_WARNING => throw new ErrorException(
$errstr, 0, $errno, $errfile, $errline
),
default => false,
};
});
Deprecations werden nur festgehalten, error_log() liefert dabei true und meldet sie damit als erledigt. Warnungen werden zur Ausnahme. Alles andere, etwa ein Hinweis aus trigger_error() mit E_USER_NOTICE, geht über false an die Standardbehandlung. Wohin die Meldung danach geschrieben wird und wie man das konfiguriert, steht im Tutorial zu error_log(). Hier geht es darum, wie die Meldung überhaupt im eigenen Code ankommt. Für eigene Testmeldungen eignen sich E_USER_WARNING und E_USER_NOTICE. E_USER_ERROR an trigger_error() zu übergeben, löst seit PHP 8.4 selbst eine Deprecation aus.
Klassenmethode als Error Handler
In größeren Anwendungen steckt der Handler meist in einer Klasse. PHP set_error_handler nimmt jede Form von callable: eine Closure, ein Array aus Objekt und Methodenname, eine statische Methode oder seit PHP 8.1 die First-Class-Callable-Syntax.
<?php
final class FehlerWandler
{
public function behandle(
int $errno,
string $errstr,
string $errfile,
int $errline
): bool {
throw new ErrorException(
$errstr, 0, $errno, $errfile, $errline
);
}
}
$wandler = new FehlerWandler();
set_error_handler($wandler->behandle(...));
/* gleichwertig: [$wandler, 'behandle'] */
Ein Detail für jede Form: Eine Warnung, die im Handler selbst entsteht, geht gemessen nicht erneut an den Handler, sondern an die Standardbehandlung. Funktionen, die selbst warnen können, gehören deshalb nur mit Bedacht in den Handler.
Grenzen des Error Handling: welche Fehler kein Handler sieht
PHP set_error_handler fängt nicht alle Fehler, und wer das annimmt, hält seinen Handler schnell für defekt. Fatale Fehler, Parse-Fehler und die Kern- und Compilerfehler erreichen ihn nie, weil PHP an dieser Stelle keinen eigenen Code mehr sicher ausführen kann. Daneben gibt es Fehler, die seit PHP 7 als Ausnahme der Klasse Error geworfen werden. Sie sind keine Meldungen mit Fehlerstufe und laufen deshalb am Error Handler vorbei, lassen sich aber mit try/catch fangen.
| Fehlerart | Beispiel | Erreicht den Handler? | Ausweg |
| E_WARNING | fehlende Datei, Undefined variable, Undefined array key, A non-numeric value encountered | ja | ErrorException werfen |
| E_NOTICE, E_DEPRECATED, alle E_USER_* | veraltete Funktion, trigger_error() | ja | festhalten oder werfen |
| E_ERROR | Allowed memory size exhausted | nein | Shutdown-Funktion mit error_get_last() |
| E_PARSE | Syntaxfehler im aufgerufenen Skript | nein | bei include: ParseError fangen |
| E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING | Fehler beim Start von PHP oder beim Kompilieren | nein | Server-Protokoll lesen |
| Error-Ausnahmen | Call to undefined function, DivisionByZeroError bei 1 % 0 | nein | catch (Error $e) oder Throwable |
Für den echten Absturz bleibt nur ein Blick nach dem Ende. register_shutdown_function() läuft auch nach einem fatalen Fehler, und error_get_last() liefert dort Typ und Meldung. Gemessen mit einem zu knappen memory_limit: Der Error Handler schweigt, die Shutdown-Funktion sieht Typ 1 und den Text Allowed memory size of 8388608 bytes exhausted.
<?php
register_shutdown_function(function (): void {
$fehler = error_get_last();
if ($fehler === null) {
return;
}
if ($fehler['type'] === E_ERROR) {
error_log('Absturz: ' . $fehler['message']);
}
});
set_error_handler() und set_exception_handler()
Die Namen klingen ähnlich, die Aufgaben sind verschieden. Der mit PHP set_error_handler registrierte Handler sieht Meldungen mit Fehlerstufe, und zwar sofort, an der Stelle, an der sie entstehen. Der Exception Handler aus set_exception_handler() sieht nur Ausnahmen, die niemand gefangen hat, und zwar ganz am Ende, kurz bevor das Skript abbricht. Beide arbeiten zusammen: Eine ErrorException, die der Error Handler wirft und kein catch abfängt, landet als nicht gefangene Ausnahme beim Exception Handler.
Fazit
Warnungen sind in PHP keine Ausnahmen, und ein try/catch allein fängt sie nicht. PHP set_error_handler schließt diese Lücke mit wenigen Zeilen: Maske prüfen, damit das @ wirkt, dann eine ErrorException werfen. Ein guter Handler endet mit einem bewussten return, weil nur false die Standardbehandlung weiterlaufen lässt. Und wer ihn nur vorübergehend setzt, nimmt ihn im finally mit restore_error_handler() wieder zurück. Fatale Fehler bleiben draußen, für sie gibt es die Shutdown-Funktion.