mitmario.dev

Ausnahmen fangen

PHP Sandbox 4 Min Lesezeit 5 BeispieleLektion 2 von 6

Ein try-Block sagt: Hier drin kann etwas schiefgehen, und wenn es das tut, mach bitte nicht das ganze Programm kaputt. Der catch-Block dahinter sagt, was stattdessen passieren soll.

Möglichst wenig in den try-Block

Ein kaputter Wert, und der Rest läuft weiter
<?php

$eingaben = ["12", "acht", "30"];

foreach ($eingaben as $eingabe) {
    try {
        if (!is_numeric($eingabe)) {
            throw new InvalidArgumentException("keine Zahl: " . $eingabe);
        }
        echo "Alter: ", (int) $eingabe, "\n";
    } catch (InvalidArgumentException $fehler) {
        echo "Uebersprungen, ", $fehler->getMessage(), "\n";
    }
}

echo "Fertig.\n";

Drei Eingaben, eine davon ist Text. Ohne try wäre nach der zweiten Schluss. Mit try bekommt die zweite eine eigene Behandlung, und die dritte kommt trotzdem noch dran.

Sieh dir an, wo der try-Block anfängt und aufhört: Er sitzt in der Schleife, nicht darum herum. Das ist der wichtigste Handgriff dieser Lektion. Läge er außen, würde die kaputte zweite Eingabe die ganze Schleife beenden, und die 30 käme nie an die Reihe.

Daraus die Regel: In den try-Block gehört so wenig wie möglich. Nur die Zeilen, die wirklich schiefgehen können, und nichts, was danach sowieso noch passieren soll. Ein try, das eine halbe Datei umschließt, fängt am Ende Fehler, an die beim Schreiben niemand gedacht hat, und behandelt sie alle gleich.

throw wirft übrigens selbst eine Ausnahme, und new InvalidArgumentException(...) legt sie an. Das sieht nach Objekten aus und ist auch welche; für diese Lektion reicht dir, dass hinter new ein Name steht und in den Klammern die Meldung. Wann du selbst wirfst, ist der Stoff von Lektion 7.4.

finally läuft, egal was passiert

finally läuft in jedem Fall
<?php

function lies(string $was): string
{
    echo "  Datei geoeffnet\n";
    try {
        if ($was === "kaputt") {
            throw new RuntimeException("Datei ist kaputt");
        }
        return "Inhalt von " . $was;
    } finally {
        echo "  Datei geschlossen\n";
    }
}

echo "Erster Versuch:\n";
echo lies("brief.txt"), "\n";

echo "Zweiter Versuch:\n";
try {
    echo lies("kaputt"), "\n";
} catch (RuntimeException $fehler) {
    echo "Gefangen: ", $fehler->getMessage(), "\n";
}

An try darf hinten ein finally hängen, und dessen Block läuft immer. Auch dann, wenn der try-Block mit return verlassen wird. Auch dann, wenn eine Ausnahme durchfliegt und weiter oben gefangen wird.

Sieh dir die Reihenfolge im ersten Versuch an: Datei geschlossen steht vor dem Inhalt. Die Funktion hat ihren Rückgabewert schon fertig, führt aber das finally noch aus, bevor der Wert beim Aufrufer ankommt. Im zweiten Versuch fliegt eine Ausnahme aus dem try heraus, und das finally läuft trotzdem. Wenn du das Zeile für Zeile sehen willst, nimm den Reiter Debug und klick auf Aufzeichnen: Nach der return-Zeile kommt noch einmal die Zeile mit Datei geschlossen, und erst dann geht es beim Aufrufer weiter.

Dafür ist es da: aufräumen. Eine Datei schließen, eine Verbindung beenden, eine Sperre lösen. Alles, was passieren muss, egal wie die Sache ausgeht.

Ein return in den finally-Block zu schreiben ist übrigens möglich und überschreibt dann das return aus dem try. Tu es nicht. Es liest sich wie ein Zauberkunststück und niemand rechnet damit.

Was du aus einer Ausnahme herausholst

Die Meldung oder die ganze Ausnahme
<?php

try {
    throw new RuntimeException("Die Datenbank antwortet nicht");
} catch (RuntimeException $fehler) {
    echo "Nur die Meldung:\n";
    echo $fehler->getMessage(), "\n";

    echo "\nDie ganze Ausnahme:\n";
    echo $fehler, "\n";
}

echo "\nUnd eine Meldung, die selbst einen Pfad enthaelt:\n";

function alterVon(int $jahr): int
{
    return 2026 - $jahr;
}

try {
    alterVon("neunzehn");
} catch (TypeError $fehler) {
    echo $fehler->getMessage(), "\n";
}

getMessage() gibt dir den Text, mit dem die Ausnahme angelegt wurde. Das ist fast immer das, was du willst.

Was du nicht willst, steht darunter. Gibst du die Ausnahme selbst aus, bekommst du den Klassennamen, die Meldung, den vollen Pfad der Datei, die Zeilennummer und einen Stack trace obendrauf. In diesem Kurs ist das /vercel/sandbox/lern/index.php, auf einem echten Server ist es der Pfad, unter dem deine Anwendung liegt. Das ist eine Information, die niemand von außen bekommen sollte, und in Abschnitt 15 siehst du, was sich damit anfangen lässt.

Der letzte Teil des Beispiels ist die Einschränkung dazu, und sie ist wichtig: Bei einem TypeError steht der Pfad in der Meldung selbst. getMessage() ist also die bessere Wahl, aber keine Zusage. Die einzige verlässliche Regel lautet: Was der Benutzer sieht, schreibst du selbst. Die Einzelheiten gehen ins Protokoll, und wie das geht, steht in Lektion 7.5.

Der Block, der nichts tut

Ein leerer catch ist schlimmer als kein catch
<?php

function preisInCent(string $eingabe): int
{
    try {
        if (!is_numeric($eingabe)) {
            throw new InvalidArgumentException("Das ist keine Zahl");
        }
        return (int) round((float) $eingabe * 100);
    } catch (InvalidArgumentException $fehler) {
        // Hier steht nichts. Genau das ist das Problem.
    }

    return 0;
}

echo "Ein Preis: ", preisInCent("24.99"), "\n";
echo "Ein Tippfehler: ", preisInCent("24,99"), "\n";
echo "Beide Zeilen sehen gleich vertrauenswuerdig aus.\n";

Ein catch-Block ohne Inhalt fängt die Ausnahme und schweigt. Das Programm läuft weiter, als wäre nichts gewesen, liefert 0 und sieht dabei genauso aus wie ein richtiges Ergebnis.

Vergleich das mit dem Zustand ohne try: Dann wäre das Programm abgestürzt, und jemand hätte den Fehler gefunden. Der leere catch hat aus einem lauten Problem ein leises gemacht, und das ist dieselbe Verschlechterung wie die Warnung aus Lektion 7.1.

Wenn du wirklich nichts tun willst, schreib wenigstens hin, warum. Ein Kommentar im catch-Block ist das Mindeste, eine Meldung auf dem Fehlerkanal ist besser.

Error und Exception sind zwei Familien

Exception fängt keinen Error
<?php

echo "Erster Versuch, mit Exception:\n";
try {
    try {
        intdiv(100, 0);
    } catch (Exception $fehler) {
        echo "  gefangen als Exception\n";
    }
} catch (Throwable $fehler) {
    echo "  durchgeflogen, es war ein ", get_class($fehler), "\n";
}

echo "Zweiter Versuch, mit Throwable:\n";
try {
    intdiv(100, 0);
} catch (Throwable $fehler) {
    echo "  gefangen als Throwable\n";
}

catch (Exception $fehler) fängt nur Ausnahmen aus der Exception-Familie. Ein DivisionByZeroError gehört nicht dazu, er ist ein Error, und der fliegt an so einem catch vorbei, als stünde es gar nicht da.

Beide Familien haben ein gemeinsames Dach: Throwable. Wer wirklich alles fangen will, fängt Throwable.

Für den Alltag trotzdem die umgekehrte Empfehlung: Fang so eng wie möglich. Exception für Dinge, mit denen du rechnest, und ein eigener Typ, sobald du zwei Fälle unterscheiden willst. Genau darum geht es in der nächsten Lektion.

Zum Mitnehmen

Bis hierhin war ein Fehler etwas, das dir passiert. Ab jetzt ist er etwas, worüber du entscheidest. Drei Schlüsselwörter reichen dafür, und das dritte davon ist das, das die meisten falsch einschätzen.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.

Was in dieser Lektion steckt

  • Artikel mit 5 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.