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?teileist 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?
- Kiste versucht den Code im ersten Block auszuführen
- Wenn dort ein Fehler geworfen wird, springt Kiste sofort zum
fange-Block - Die Variable
fehlerenthält den geworfenen Wert - 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 Fehler — sag 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 Feldzugriffe.nachricht/e.zeile/e.spaltefunktionieren jetzt auch im kompilierten.exe-Pfad (für die eingebauten Fehler). Fängst du dagegen eine eigene geworfene Klasse (wirf neu MeineKlasse(...)), lieferne.nachricht/e.zeile/e.spalteim 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 mitversuche/fangeabfangen. 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
/ina / 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 individierein Zeile 2.in berechne (test.ki:6)—berechneriefdividierein Zeile 6 auf.in <top-level> (test.ki:9)— die äußerste Ebene riefberechnein 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
nichtszurü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:
-
wirfbraucht eine Klassen-Instanz —wirf "text"oderwirf 42werden vom Compiler abgelehnt. Workaround: eigene Fehler-Klasse definieren (klasse MeinFehler { funktion neu(msg) { dies.msg = msg } }) und davon eine Instanz werfen. -
gib/springe/weiterimversuche-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 } -
Member-Access auf catch-Variable: nur die eingebauten Fehler-Felder —
fange e { sag(e.nachricht) }(sowiee.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. -
Nested
fangebraucht unterschiedliche Variablennamen —versuche { versuche { ... } fange e { ... } } fange e { ... }wird abgelehnt. Workaround: andere Namen verwenden (z.B.innenundaussen). -
Uncaught User-Throws zeigen generische Meldung — wenn ein
wirf neu MeinFehler(...)ausmainrausläuft ohne gefangen zu werden, ist die stderr-Ausgabe aktuell"Unbehandelte Klassen-Instance geworfen"+ exit-Code 1, nicht derals_text()der Klasse. Bis dahin: alle User-Throws imversuche/fangebehandeln. -
schliesslich-Block existiert nicht — Cleanup-Code muss du infange e { ... }schreiben, optional kombiniert mit einer Variable die markiert ob alles glatt ging. -
fangeinnerhalb einer Schleife (solange/wiederhole) wird abgelehnt (die fange-Variable würde pro Durchlauf neu eingeführt). Workaround: dieversuche/fange-Logik in eine eigene Funktion auslagern und diese in der Schleife aufrufen. -
gibimversuche-Body wird noch nicht unterstützt. Workaround (nennt der Compiler auch):nimm ergebnis = <wert>imversuche, nach dem Blockgib ergebnis.
Neu (2026-06-08): Text → Zahl im Build.
als_ganz("42"),als_komma("19.95")undals_dezimal("2.50")funktionieren jetzt auch imkiste build-Pfad (vorher nurkiste 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.