Kiste
EN

Fehlerbehandlung

Verfügbar im Tree-Walker (kiste run) seit Phase 3, und im nativen Build-Pfad (kiste build) seit Phase 9.2.E (2026-05-12) — mit kleinen Subset-Einschränkungen, siehe §11.8.

Manchmal geht etwas schief: eine Datei existiert nicht, eine Zahl ist ungültig, eine Division durch Null. Dafür gibt es Fehlerbehandlung.

Kiste verwendet drei Keywords: wirf, versuche und fange.

11.1 Einen Fehler werfen — wirf

Wenn deine Funktion auf etwas Unmögliches stößt, wirfst du einen Fehler:

funktion dividiere(a, b) {
    wenn b == 0 {
        wirf "Division durch Null nicht erlaubt"
    }
    gib a / b
}

Warum nicht teile? teile ist ein reserviertes Schlüsselwort (es exportiert Dinge aus einem Modul, siehe Kapitel 12) — du kannst es nicht als Funktionsnamen verwenden. Reservierte Wörter findest du im Anhang (Kapitel 38).

Was passiert dann?

  • Die Funktion wird sofort beendet
  • Der Fehler wandert nach oben durch alle Funktionen, die diese Funktion aufgerufen haben
  • Wenn niemand den Fehler abfängt, stürzt das Programm ab mit Fehlermeldung

11.2 Einen Fehler abfangen — versuche / fange

Wenn du Code hast, der vielleicht einen Fehler wirft, packst du ihn in einen versuche-Block:

versuche {
    nimm ergebnis = dividiere(10, 0)
    sag "Ergebnis: {ergebnis}"
} fange fehler {
    sag "Etwas ging schief: {fehler}"
}

Was passiert hier?

  1. Kiste versucht den Code im ersten Block auszuführen
  2. Wenn dort ein Fehler geworfen wird, springt Kiste sofort zum fange-Block
  3. Die Variable fehler enthält den geworfenen Wert
  4. Geht alles gut → der fange-Block wird übersprungen

Ausgabe: Etwas ging schief: Division durch Null nicht erlaubt

11.3 Eingebaute Fehler — wichtig zu wissen

Manche Operationen lösen automatisch einen Fehler aus, z.B.:

sag 10 / 0                     // Division durch Null
nimm liste = [1, 2, 3]
sag liste[99]                  // Listen-Index außerhalb der Länge
nimm zahl = als_ganz("abc")    // kann "abc" nicht in ganz umwandeln

Diese eingebauten Fehler kannst du genauso mit versuche/fange abfangen wie selbst geworfene (wirf) — in allen Backends gleich (kiste run, VM und kompilierte .exe):

versuche {
    nimm zahl = als_ganz(eingabe)   // wirft, wenn die Eingabe keine Zahl ist
    sag "Du hast eingegeben: {zahl}"
} fange e {
    sag "Das war keine gültige Zahl."
    sag e                            // lesbare Fehlermeldung
}

Der gefangene Wert e ist ein Fehlersag e (oder "{e}") zeigt die Meldung, und du kannst e.nachricht, e.zeile, e.spalte lesen:

versuche {
    nimm wert = liste[99]
} fange e {
    sag "Fehler: {e.nachricht} (Zeile {e.zeile})"
}

Hinweis (nativer Pfad): sag e / "{e}" und der Feldzugriff e.nachricht / e.zeile / e.spalte funktionieren jetzt auch im kompilierten .exe-Pfad (für die eingebauten Fehler). Fängst du dagegen eine eigene geworfene Klasse (wirf neu MeineKlasse(...)), liefern e.nachricht/e.zeile/e.spalte im Build vorerst "<Fehler>"/0/0 — deren eigene Felder zu lesen kommt mit Typ-Verfeinerung in einer späteren Phase. Welche Fehler nicht fangbar sind: echte Programmierfehler (Tippfehler im Variablennamen, Typfehler) — die sollen auffallen, nicht verschluckt werden.

Trotzdem gilt als guter Stil: wo es einfach ist, prüfe vorher (wenn b == 0 { … }) statt erst hinterher zu fangen — das macht die Absicht klarer. Fangen ist da, wo Vorab-Prüfen unpraktisch ist (Datei-Operationen, Eingabe-Umwandlung, Netz).

Merke: eingebaute Fehler (Division durch Null, Index außerhalb der Liste, ungültige Umwandlung, Datei-/Netz-Fehler) und eigene Fehler (wirf) → beide mit versuche/fange abfangen. Nur Programmierfehler (unbekannte Variable, Typfehler) brechen ab.

11.3.5 Stacktraces — wenn ein Fehler nicht gefangen wird

Wenn dein Programm einen Fehler nicht mit versuche/fange abfängt, beendet Kiste das Programm und gibt eine deutsche Fehlermeldung samt Stacktrace auf stderr aus:

funktion dividiere(a, b) {
    gib a / b              // Zeile 2
}

funktion berechne(x) {
    gib dividiere(x, 0)    // Zeile 6
}

sag(berechne(10))          // Zeile 9

Ausgabe:

test.ki:2:11: Division durch Null
Stacktrace:
  in dividiere (test.ki:2)
  in berechne (test.ki:6)
  in <top-level> (test.ki:9)

So liest du den Trace:

  • Erste Zeile: wo der Fehler tatsächlich aufgetreten ist (Zeile 2, Spalte 11 — das / in a / b).
  • Stacktrace:-Block: der Call-Pfad innen-nach-außen — jede Zeile zeigt die Stelle innerhalb der jeweiligen Funktion, an der es weiterging:
    • in dividiere (test.ki:2) — der Fehler entstand in dividiere in Zeile 2.
    • in berechne (test.ki:6)berechne rief dividiere in Zeile 6 auf.
    • in <top-level> (test.ki:9) — die äußerste Ebene rief berechne in Zeile 9 auf.

Stacktraces sind dein wichtigstes Debug-Werkzeug. Sie sagen dir nicht nur was schief gelaufen ist, sondern auch wer den Fehler ausgelöst hat. Lies sie immer von oben nach unten: die erste Zeile ist die Fehler-Stelle, die folgenden Zeilen zeigen wie du dort hingekommen bist.

In allen drei Backends gleich: Tree-Walker (kiste run), VM (kiste run --vm) und native (kiste build) erzeugen byte-identische Stacktraces — das ist eine bewusste Design-Garantie (Phase B.8.6, E-090).

Schutz vor Stack-Overflow: wenn eine Funktion sich endlos selbst aufruft (Endlos-Rekursion), bricht Kiste nach ~10.000 Frames mit dem Fehler Stack-Überlauf — zu tiefe Rekursion ab. Das ist ein Schutz, kein Bug.

11.4 Verschachtelte Fehler — Re-Throw

Manchmal willst du einen Fehler abfangen, etwas damit machen, und dann weiterwerfen:

versuche {
    versuche {
        wirf "innerer Fehler"
    } fange e {
        sag "Habe gesehen: " + e
        wirf "Ich werfe weiter: " + e
    }
} fange e {
    sag "Außen gefangen: " + e
}

Ausgabe:

Habe gesehen: innerer Fehler
Außen gefangen: Ich werfe weiter: innerer Fehler

11.5 Praktisches Beispiel

Ein typisches Muster: eine Funktion prüft ihre Eingaben und wirft bei Verstoß einen Fehler; der Aufrufer fängt ihn mit versuche/fange. (Eingebaute Fehler — Division/0, Index-OOB, Umwandlung — werden vom selben versuche/fange mitgefangen, siehe §11.3.)

funktion abheben(kontostand, betrag) {
    wenn betrag <= 0 {
        wirf "Betrag muss positiv sein"
    }
    wenn betrag > kontostand {
        wirf "Nicht genug Guthaben (Stand: {kontostand})"
    }
    gib kontostand - betrag
}

versuche {
    nimm rest = abheben(100, 150)
    sag "Neuer Stand: {rest}"
} fange fehler {
    sag "Abhebung fehlgeschlagen: {fehler}"
}

Ausgabe: Abhebung fehlgeschlagen: Nicht genug Guthaben (Stand: 100)

11.6 Wann werfe ich Fehler? Tipps für die Praxis

Werfen, wenn:

  • Eine Vorbedingung nicht erfüllt ist (z.B. negative Zahl bei Wurzelfunktion)
  • Eine externe Ressource nicht verfügbar ist (Datei fehlt)
  • Eine Eingabe formal ungültig ist (Text statt Zahl)

NICHT werfen für:

  • Normale Programm-Ablaufsteuerung (dafür wenn/sonst)
  • Erwartete "Negativ-Ergebnisse" (z.B. "Element nicht gefunden" — gib lieber nichts zurück)

Faustregel: Fehler sind für außergewöhnliche Situationen — Dinge, die "eigentlich nicht passieren sollten".

11.7 Fehler-Klassen — Strukturierte Fehler

Du kannst auch eigene Fehler-Klassen werfen, nicht nur Texte. So bekommst du mehr Kontext:

klasse DateiFehler {
    funktion neu(pfad, grund) {
        dies.pfad = pfad
        dies.grund = grund
    }
}

versuche {
    wirf neu DateiFehler("/tmp/test.txt", "nicht gefunden")
} fange f {
    sag "Fehler bei " + f.pfad + ": " + f.grund
}

Im Tree-Walker (kiste run) sind beliebige Werte werfbar — Texte, Zahlen, Klassen-Instanzen. Im nativen Build-Pfad (kiste build) gibt es Subset-Einschränkungen — siehe §11.8.

11.8 Fehlerbehandlung im kiste build-Pfad — aktueller Subset

Hinweis: Dieser Abschnitt beschreibt den nativen Build-Pfad (kiste build → .exe). Im Tree-Walker (kiste run) gelten die obigen Regeln ohne Subset-Einschränkungen.

Phase 9.2.E (E.0 + E.1 + E.2, 2026-05-12) bringt versuche / fange / wirf in den nativen Pfad. Damit fangen kompilierte .exe-Programme jetzt auch Runtime-Fehler ab und können selbst Klassen werfen.

Was funktioniert

klasse MeinFehler {
    funktion neu(msg) { dies.msg = msg }
}

versuche {
    nimm liste = [1, 2, 3]
    sag(liste[99])           // wirft Index-OOB
} fange e {
    sag("OOB gefangen")
}

versuche {
    wirf neu MeinFehler("bums")
} fange e {
    sag("User-Throw gefangen")
}

// Re-Throw via `wirf e`:
versuche {
    versuche {
        wirf neu MeinFehler("inner")
    } fange innen {
        sag("inner sah")
        wirf innen           // weiterwerfen
    }
} fange aussen {
    sag("outer gefangen")
}

Was im LLVM-Pfad gefangen wird:

  • Runtime-Fehler: Ganz-Überlauf, Division durch Null, Index außerhalb der Liste, stdlib-Fehler (Datei/JSON/CSV/…), Umwandlungs-/Parse-Fehler.
  • User-Throws: wirf neu DeineKlasse(...) für jede selbst-definierte Klasse.

run/build-Parität (2026-06-04): Tree-Walker (kiste run) und VM fangen diese eingebauten Fehler jetzt genauso wie der native Build — vorher brachen sie ab. sag(e) zeigt in allen drei Backends eine lesbare Meldung (der genaue Text ist je Backend etwas anders). Nur Programmierfehler (unbekannte Variable, Typfehler) bleiben absichtlich nicht fangbar. Details: docs/fehler-paritaet.md.

Aktuelle Subset-Einschränkungen (Stand 2026-05-12)

Diese Einschränkungen gibt es nur im kiste build-Pfad, nicht im Tree-Walker:

  1. wirf braucht eine Klassen-Instanzwirf "text" oder wirf 42 werden vom Compiler abgelehnt. Workaround: eigene Fehler-Klasse definieren (klasse MeinFehler { funktion neu(msg) { dies.msg = msg } }) und davon eine Instanz werfen.

  2. gib / springe / weiter im versuche-Body nicht erlaubt — der Compiler lehnt sie ab mit klarer Workaround-Meldung. Beispiel:

    funktion test() {
        versuche {
            gib 1                // Compile-Error im kiste build
        } fange e {
            gib 0
        }
    }

    Workaround: Wert in einer Variable speichern und nach dem versuche-Block zurückgeben:

    funktion test() {
        nimm ergebnis = 0
        versuche {
            ergebnis = 1
        } fange e {
            ergebnis = 0
        }
        gib ergebnis
    }
  3. Member-Access auf catch-Variable: nur die eingebauten Fehler-Felderfange e { sag(e.nachricht) } (sowie e.zeile / e.spalte) funktioniert jetzt auch im Build für die eingebauten Fehler. Fängst du eine eigene geworfene Klasse, liefern diese drei Felder im Build vorerst "<Fehler>"/0/0 (deren konkreter Typ ist zur Compile-Zeit unbekannt); ihre eigenen Felder zu lesen kommt mit Typ-Verfeinerung (wenn typ(e) == MeineKlasse { ... }) in einer späteren Phase. Im Tree-Walker/VM geht der Feldzugriff auf eigene Klassen schon heute.

  4. Nested fange braucht unterschiedliche Variablennamenversuche { versuche { ... } fange e { ... } } fange e { ... } wird abgelehnt. Workaround: andere Namen verwenden (z.B. innen und aussen).

  5. Uncaught User-Throws zeigen generische Meldung — wenn ein wirf neu MeinFehler(...) aus main rausläuft ohne gefangen zu werden, ist die stderr-Ausgabe aktuell "Unbehandelte Klassen-Instance geworfen" + exit-Code 1, nicht der als_text() der Klasse. Bis dahin: alle User-Throws im versuche/fange behandeln.

  6. schliesslich-Block existiert nicht — Cleanup-Code muss du in fange e { ... } schreiben, optional kombiniert mit einer Variable die markiert ob alles glatt ging.

  7. fange innerhalb einer Schleife (solange/wiederhole) wird abgelehnt (die fange-Variable würde pro Durchlauf neu eingeführt). Workaround: die versuche/fange-Logik in eine eigene Funktion auslagern und diese in der Schleife aufrufen.

  8. gib im versuche-Body wird noch nicht unterstützt. Workaround (nennt der Compiler auch): nimm ergebnis = <wert> im versuche, nach dem Block gib ergebnis.

Neu (2026-06-08): Text → Zahl im Build. als_ganz("42"), als_komma("19.95") und als_dezimal("2.50") funktionieren jetzt auch im kiste build-Pfad (vorher nur kiste run). Ungültiger Text (als_komma("abc")) wirft einen fangbaren Fehler — ideal für getippte Eingaben in GUI-Apps. (Auch numerische Eingaben: als_komma(10), als_ganz(3.9) usw.)

Diese Einschränkungen sind bewusste Subset-Disziplin und werden in späteren Sub-Phasen (Polish für 9.2.E) gelift. Für heutige Production-Apps reicht der aktuelle Subset für die meisten Use-Cases — CLI-Tools, GUI-Event-Handler, Batch-Processoren.

Welcher Pfad ist für dich richtig?

  • Tree-Walker (kiste run): voller Sprach-Subset, langsamer (*big.Int-Arithmetik). Gut für Skripte, REPL, Entwicklung.
  • Native Build (kiste build → .exe): schneller (Lua/JS-Niveau), aktuell aber mit Subset-Einschränkungen. Gut für Production-Apps wenn die obigen Einschränkungen keinen blocker darstellen.

In den meisten Fällen entwickelst du im Tree-Walker und buildst final mit kiste build. Wenn der Build wegen Subset-Einschränkungen failt, gibt dir der Compiler eine deutsche Fehler-Meldung mit Workaround-Hinweis.