Ein Anmeldeformular soll bei einer schon vergebenen E-Mail-Adresse keinen Datenbankauszug zeigen, sondern einen Satz, den ein Besucher versteht. Damit das gelingt, muss der Code diesen einen Fehler von allen anderen trennen, bevor er irgendetwas ausgibt. PHP mysqli_errno liefert genau die Zahl, an der sich ein doppelter Eintrag von einer fehlenden Tabelle oder einem Deadlock unterscheidet. Der Weg zur Abfrage selbst, also Verbindung, Platzhalter und Ausführung, steht im Tutorial zu Prepared Statements und wird hier vorausgesetzt.
Die Nummer ist die einzige Auskunft, die sich weder mit dem Schema noch mit der Sprache des Servers ändert. Alles andere an einem Datenbankfehler tut das sehr wohl.
Was PHP mysqli_errno() liefert
mysqli_errno() gibt die Fehlernummer der zuletzt ausgeführten Operation auf dieser Verbindung heraus. Lief alles glatt, steht dort eine Null. Der Wert hängt an der Verbindung, nicht an einer einzelnen Abfrage, und jede neue Operation überschreibt ihn. Wer die Nummer braucht, liest sie deshalb sofort aus und nicht drei Zeilen später.
Zwei Nachbarfunktionen beantworten dieselbe Frage auf andere Weise. mysqli_error() liefert den Meldungstext, mysqli_sqlstate() den genormten Zustandscode. Nach einer erfolgreichen Abfrage sehen die drei Werte so aus:
<?php
$db->query('SELECT 1');
var_dump($db->errno); /* int(0) */
var_dump($db->error); /* string(0) "" */
var_dump($db->sqlstate); /* string(5) "00000" */
Drei Sorten von Leere: die Null, der leere Text und die Zeichenkette aus fünf Nullen. Wer auf Erfolg prüft, nimmt die Null aus mysqli_errno().
Syntax, Parameter und Rückgabewert im Original
<?php
mysqli_errno(mysqli $mysql): int
mysqli_connect_errno(): int
mysqli_sqlstate(mysqli $mysql): string
/* mysql: a connection returned by
mysqli_connect() or new mysqli()
errno returns the error code of the most
recent function call, 0 on success
connect_errno takes no argument and
reports the last connection attempt
sqlstate returns the five character
ANSI SQL state, '00000' on success
object style: $db->errno, $db->sqlstate */
An der Signatur hängen zwei Punkte, die später Zeit sparen. Erstens ist der Rückgabetyp eine Ganzzahl und nie false, geprüft wird also auf Ungleichheit zu null. Zweitens verlangt PHP mysqli_errno() eine Verbindung als Parameter, während mysqli_connect_errno() ohne Argument auskommt. Genau darin liegt der Unterschied zwischen einem Fehler beim Arbeiten und einem Fehler beim Verbinden.
Seit PHP 8.1 wirft mysqli Exceptions von allein
Bis PHP 8.0 gab ein gescheitertes mysqli_query() nur false zurück. Seit PHP 8.1 ist der Meldemodus MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT voreingestellt, und ein Fehler kommt als mysqli_sql_exception. Ein Beispiel der Form if (!$ergebnis) { echo mysqli_errno($db); } erreicht seine eigene Ausgabe nie.
flowchart TD
A[Abfrage absetzen] --> B{Fehler?}
B -- nein --> C[errno = 0]
B -- ja --> D{Modus wirft?}
D -- ja --> E[mysqli_sql_exception]
E --> F[getCode]
D -- nein --> G[Rückgabe false]
G --> H[mysqli_errno]
F --> I[Nummer auswerten]
H --> I
Beide Wege führen zur selben Zahl, und das ist die gute Nachricht. An einem doppelten Primärschlüssel gemessen liefert $e->getCode() die 1062, $e->getSqlState() die Zeichenkette 23000, und PHP mysqli_errno() meldet im catch-Block ebenfalls 1062. Die Ausnahmeklasse erbt von RuntimeException, ein vorhandener Fehlerbehandler fängt sie also mit.
<?php
/* Weg 1: Ausnahme, Standard seit PHP 8.1 */
try {
$db->query($insert);
} catch (mysqli_sql_exception $e) {
echo $e->getCode(); /* 1062 */
echo $e->getSqlState(); /* 23000 */
echo mysqli_errno($db); /* auch 1062 */
}
/* Weg 2: alter Stil, Meldungen abgeschaltet */
mysqli_report(MYSQLI_REPORT_OFF);
if ($db->query($insert) === false) {
echo mysqli_errno($db); /* 1062 */
}
Welcher Weg der richtige ist, entscheidet der umgebende Code. In einer Anwendung mit zentralem Fehlerbehandler sind Ausnahmen die bessere Wahl. In einem alten Skript, das an vierzig Stellen auf false prüft, ist ein einzelnes mysqli_report(MYSQLI_REPORT_OFF) am Anfang ehrlicher als vierzig halbfertige Umbauten. PHP mysqli_errno() funktioniert in beiden Welten gleich.
Nummer, Text oder Zustandscode
Drei Quellen, drei Eignungen. Der Meldungstext eines Duplikats lautet in der Testdatenbank Duplicate entry '1' for key 'zz_t.PRIMARY' und trägt Tabellen- und Schlüsselnamen in sich. Benennt jemand die Tabelle um, passt der Vergleich nicht mehr. Schlimmer ist es bei Verbindungsfehlern: Die 2002 kommt auf einem deutschsprachigen Windows mit dem Satz „Es konnte keine Verbindung hergestellt werden“, weil dieser Text vom Betriebssystem stammt. Wer auf „Connection refused“ prüft, findet dort nichts. Die Nummer war beide Male dieselbe.
| Quelle | Was herauskommt | Bei Erfolg | Taugt zum Verzweigen? |
| mysqli_errno() | Ganzzahl, etwa 1062 | 0 | Ja. Sie bleibt über Sprachen und Schemas hinweg gleich. |
| mysqli_error() | Meldungstext mit Tabellen- und Schlüsselnamen | leerer Text | Nein. Gut fürs Protokoll, untauglich als Bedingung. |
| mysqli_sqlstate() | fünfstelliger genormter Zustandscode | '00000' | Nur grob. 1062, 1048, 1451 und 1452 tragen alle die 23000. |
Der Zustandscode wirkt präziser, weil er genormt ist, und ist doch der gröbere Wert. Vier verschiedene Lagen teilen sich die 23000: ein Duplikat, ein fehlendes Pflichtfeld, eine Vaterzeile mit Kindern und eine Kindzeile ohne Vater. Wer darauf verzweigt, behandelt alle vier gleich. Seine Stärke liegt beim Wechsel des Datenbanksystems, denn dort bleibt er stabil, während die Fehlernummern herstellerspezifisch sind.
Ins Protokoll gehören alle drei Angaben, und zwar als getrennte Felder. Wer sie zu einem Satz verschmilzt, kann später nicht mehr nach der Nummer filtern. mysqli_error_list() liefert diese drei Felder fertig als Array. Wie ein Protokoll überhaupt aufgesetzt und ausgewertet wird, steht im Tutorial zu error_log().
<?php
$eintrag = [
'errno' => mysqli_errno($db),
'sqlstate' => mysqli_sqlstate($db),
'error' => mysqli_error($db),
'sql' => $sql,
];
error_log(json_encode($eintrag));
Fehler beim Verbindungsaufbau mit mysqli_connect_errno()
Solange keine Verbindung steht, gibt es kein Objekt, das PHP mysqli_errno() entgegennehmen könnte. Dafür ist mysqli_connect_errno() zuständig. Zwei Fallen lauern hier. Erstens liefert mysqli_connect_error() nach einem erfolgreichen Aufbau nicht den leeren Text, sondern null. Zweitens gibt new mysqli() bei abgeschaltetem Meldemodus auch bei falschem Passwort ein Objekt zurück. Eine Prüfung auf if (!$db) greift dort nie.
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli($host, $user, $pass, $name);
/* 1045 falsches Passwort
1044 kein Zugriff auf diese Datenbank
2002 Server nicht erreichbar */
if ($db->connect_errno !== 0) {
error_log('DB ' . $db->connect_errno
. ': ' . $db->connect_error);
exit('Datenbank nicht erreichbar.');
}
Praxis 1: doppelter Eintrag mit 1062 abfangen
Der klassische Fall. Statt vorher zu fragen, ob die Adresse vergeben ist, schreibt der Code einfach und fängt den einen Fehler ab. Das spart eine Abfrage und schließt die Lücke zwischen Prüfung und Einfügen, in der sich zwei gleichzeitige Anmeldungen überholen. Wichtig ist der zweite Teil der Bedingung: Jede andere Nummer fliegt weiter nach oben.
<?php
try {
$stmt = $db->prepare(
'INSERT INTO nutzer (mail) VALUES (?)'
);
$stmt->bind_param('s', $mail);
$stmt->execute();
$hinweis = 'Konto angelegt.';
} catch (mysqli_sql_exception $e) {
if ($e->getCode() !== 1062) {
throw $e;
}
$hinweis = 'Diese Adresse ist vergeben.';
}
Eine Einschränkung gehört dazu: Die 1062 verrät nicht, welcher Schlüssel kollidiert ist. Gemessen melden der verletzte Primärschlüssel und der verletzte eindeutige Index dieselbe Nummer. Wer zwei eindeutige Spalten hat, etwa Adresse und Benutzername, erfährt aus PHP mysqli_errno() also nicht, welches Feld der Besucher ändern soll. Dann bleibt die gezielte Vorabprüfung oder das Auslesen des Schlüsselnamens aus der Meldung, und Letzteres bindet den Code ans Schema.
Praxis 2: INSERT-Import, der nur Dubletten überspringt
Ein nächtlicher Import soll durchlaufen, auch wenn einzelne Zeilen schon in der Tabelle stehen. Alles andere muss ihn sofort stoppen, denn ein Import, der stumm die Hälfte verwirft, ist schlimmer als gar keiner. Genau eine Nummer wird geduldet.
<?php
$uebersprungen = 0;
foreach ($zeilen as $zeile) {
$stmt->bind_param('ss', $zeile[0], $zeile[1]);
try {
$stmt->execute();
} catch (mysqli_sql_exception $e) {
if ($e->getCode() !== 1062) {
throw $e;
}
$uebersprungen++;
}
}
echo "$uebersprungen Dubletten ignoriert";
Vorbereitete Anweisungen haben eigene Auskunftsfunktionen. Nach einem gescheiterten mysqli_stmt_execute() liefern mysqli_stmt_errno($stmt) und mysqli_errno($db) denselben Wert, hier 1062. Dasselbe gilt für mysqli_stmt_sqlstate() gegenüber mysqli_sqlstate(). Der Unterschied zählt trotzdem, denn mysqli_prepare() kann selbst scheitern, und dann gibt es noch kein Statement. Gemessen steht dort 1146 an der Verbindung bereit.
Praxis 3: Deadlock erkennen und wiederholen
Manche Fehler bedeuten nicht, dass der Vorgang falsch war, sondern dass er Pech hatte. Zwei Verbindungen sperren zwei Zeilen in umgekehrter Reihenfolge, der Server erklärt eine zum Opfer und bricht sie ab. Gemessen mit zwei Verbindungen auf InnoDB: die eine bekommt 1213 mit Zustandscode 40001 und dem Text Deadlock found when trying to get lock; try restarting transaction, die andere läuft mit einer Fehlernummer von 0 durch. Verwandt damit ist 1205 mit Zustandscode HY000, die Sperrzeitüberschreitung. Wie ein Vorgang überhaupt umschlossen wird, steht im Tutorial zu beginTransaction, commit und rollBack.
<?php
$wiederholbar = [1213, 1205];
for ($versuch = 1; $versuch <= 3; $versuch++) {
try {
$db->begin_transaction();
buchen($db, $von, $nach, $betrag);
$db->commit();
break;
} catch (mysqli_sql_exception $e) {
$db->rollback();
$code = $e->getCode();
if (!in_array($code, $wiederholbar, true)) {
throw $e;
}
usleep(50000 * $versuch);
}
}
Die Liste der wiederholbaren Nummern muss kurz bleiben. Ein Syntaxfehler ist beim dritten Versuch derselbe Syntaxfehler, und eine Schleife, die jede Fehlernummer wiederholt, verwandelt einen Tippfehler in Dauerlast. Die wachsende Wartezeit ist kein Schmuck: Ohne sie treffen die beiden Verbindungen sofort wieder aufeinander.
Die Nummern aus dem Alltag
Nummern unter 2000 kommen vom MySQL-Server, Nummern ab 2000 von der Clientbibliothek. Das erklärt, warum 2006 kein SQL-Fehler ist: Die Abfrage war in Ordnung, die Leitung nicht. Alle Werte sind an PHP 8.4 gegen MySQL 8.0.46 nachgemessen.
| Nummer | SQLSTATE | Bedeutung | Sinnvolle Reaktion |
| 1062 | 23000 | Primärschlüssel oder eindeutiger Index verletzt | Besucher informieren |
| 1048 | 23000 | NULL in einer Pflichtspalte | Eingabeprüfung nachziehen |
| 1451 | 23000 | Vaterzeile hat noch Kinder | Abhängige Datensätze zuerst |
| 1452 | 23000 | Kindzeile ohne passenden Vater | Fremdschlüsselwert prüfen |
| 1146 | 42S02 | Tabelle gibt es nicht | Migration fehlt |
| 1213 | 40001 | Deadlock, Transaktion abgebrochen | Wiederholen |
| 1205 | HY000 | Sperrzeitüberschreitung beim Warten | Wiederholen |
| 2002 | HY000 | Server nicht erreichbar, Meldung der Clientbibliothek | Wartungsseite statt Fehlerausgabe |
| 2006 | HY000 | Verbindung weggebrochen | Neu verbinden |
Beim Umstieg auf PDO lauert hier eine Vertauschung. PDOStatement::errorInfo() liefert ein Array, in dem an Position 0 der Zustandscode steht und an Position 1 die treiberspezifische Nummer, also das Gegenstück zu PHP mysqli_errno(). PDOException::getCode() gibt dagegen den Zustandscode heraus. Wer eine Verzweigung auf 1062 eins zu eins überträgt, vergleicht dort plötzlich gegen 23000. Mehr dazu im Vergleich von PDO und mysqli.
Was PHP mysqli_errno() nicht sieht
Die Funktion meldet Fehler, keine Auffälligkeiten. Das ist eine echte Grenze, und sie fällt genau dann auf, wenn Daten leise falsch in der Tabelle landen. Gemessen: SELECT CAST('abc' AS SIGNED) läuft mit einer Fehlernummer von 0 durch, obwohl MySQL den Wert verwirft. Die Auskunft steckt in mysqli_warning_count() und mysqli_get_warnings().
<?php
$db->query("SELECT CAST('abc' AS SIGNED)");
echo mysqli_errno($db); /* 0 */
echo mysqli_warning_count($db); /* 1 */
$w = mysqli_get_warnings($db);
echo $w->errno; /* 1292 */
echo $w->message;
/* Truncated incorrect INTEGER value: 'abc' */
Der zweite blinde Fleck wird gern falsch gedeutet. Ein UPDATE, das keine Zeile trifft, liefert 0 betroffene Zeilen bei einer Fehlernummer von 0. Das ist kein Fehler, sondern kein Treffer. Die dritte Falle liegt beim Aufräumen: Nach mysqli_close() wirft PHP mysqli_errno() den harten Fehler mysqli object is already closed. Wer im finally-Block schließt und danach protokolliert, baut sich genau das ein.
Fazit
Die Fehlernummer ist die einzige Auskunft, die sich nicht ändert, wenn jemand eine Spalte umbenennt oder der Server in einer anderen Sprache antwortet. PHP mysqli_errno() holt sie in einer Zeile, im catch-Block genauso wie nach einem false. Drei Bausteine daraus tragen weit: die freundliche Meldung bei 1062, der Import mit genau einer geduldeten Nummer und die Wiederholschleife für 1213 und 1205. Warnungen holt man sich getrennt, denn die sieht PHP mysqli_errno() nicht.