Menu

Zero Raises und Check: Explizite Fehler ohne Exceptions

Zero-Funktionen deklarieren ihre Fehlerarten mit raises und Aufrufer bestätigen sie mit check. So funktioniert das System, warum es kein stilles Werfen gibt und wie es mit der World-Capability zusammenspielt.

Diese Seite enthält ausführbare Editoren — bearbeiten, ausführen und Ausgabe sofort sehen.

Der Aufbau

Fehler sind in Zero kein eigener, paralleler Kontrollfluss. Sie sind Teil des regulären Kontrollflusses, in der Funktionssignatur deklariert und an jeder Aufrufstelle bestätigt. Zwei Teile bringen das zum Funktionieren:

  • raises in der Funktionssignatur – „diese Funktion kann fehlschlagen“.
  • check an der Aufrufstelle – „wenn das fehlschlägt, lass meine Funktion mit demselben Fehler fehlschlagen“.

Die Kombination reicht aus, um das auszudrücken, wofür die meisten Sprachen zu try/catch oder Result-Typen greifen.

Eine fehlbare Funktion deklarieren

Hänge raises nach den Rückgabetyp:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

Zwei Lesarten der Signatur:

  • validate gibt einen i32 zurück oder wirft InvalidInput.“
  • „Die Menge der möglichen Ergebnisse ist { i32, InvalidInput }.“

Beide Lesarten sind richtig. Der Compiler verfolgt beide Möglichkeiten und verlangt, dass Aufrufer mit jeder etwas Explizites tun.

Du kannst auch eine bloße raises-Klausel schreiben:

pub fun main(world: World) -> Void raises {
    check world.out.write("hello\n")
}

raises (ohne Fehlerliste) heißt „diese Funktion kann mit jedem Fehler fehlschlagen“. Bei main ist das die konventionelle Form – das Programm kann mit einem Exit-Code ungleich Null beenden, falls etwas schiefgeht, und die Runtime kümmert sich darum, den Fehler sichtbar zu machen.

Für Funktionen tiefer im Aufrufstack bevorzuge die explizite Form raises { FehlerA, FehlerB }, damit die Fehlerarten auf jeder Ebene dokumentiert sind.

Einen Fehler werfen

In einer fehlbaren Funktion verlässt raise die Funktion mit dem gegebenen Fehler:

fun validate(ok: Bool) -> i32 raises { InvalidInput } {
    if ok == false {
        raise InvalidInput
    }
    return 42
}

raise InvalidInput erzeugt an dieser Zeile einen InvalidInput-Fehler. Die Funktion läuft an diesem Punkt nicht weiter – die Kontrolle kehrt zum Aufrufer zurück, und der Aufrufer sieht den Fehler statt eines i32. Die raises-Klausel listet die einzigen Fehlertypen, die diese Funktion werfen darf; etwas zu werfen, das nicht in der Liste steht, ist ein Compile-Fehler.

Einen Fehler mit check propagieren

Ein Aufrufer, der eine fehlbare Funktion aufruft, muss den möglichen Fehlschlag bestätigen. Die häufigste Bestätigung ist check:

fun run() -> Void raises { InvalidInput } {
    check validate(true)
}

check validate(true) macht zwei Dinge:

  1. Ruft validate(true) auf.
  2. Falls validate einen Fehler geworfen hat, propagiert es ihn nach oben – run wirft denselben Fehler an seinen Aufrufer.

Damit die Propagierung erlaubt ist, muss run in seiner eigenen raises-Klausel deklarieren, dass es InvalidInput (oder etwas Kompatibles) werfen kann. Der Compiler prüft das. Stünde in runs Signatur raises { OtherError }, würde die Propagierung nicht kompilieren, weil InvalidInput nicht im Set ist.

Ein komplettes durchgespieltes Beispiel aus den offiziellen Samples der Sprache – klick Run, um die Propagierung erfolgreich zu sehen:

Der Fehlertyp wandert zusammen mit der Funktionssignatur den ganzen Aufrufstack hoch. mains bloßes raises akzeptiert alles, was run werfen könnte, also landet die Propagierung sicher.

Warum nicht try/catch?

Die Designdisziplin hinter raises/check ist, dass Fehler an einer Aufrufstelle nie unsichtbar sind. In einer try/catch-Sprache kann eine Exception stillschweigend durch eine Funktion ziehen, die nicht einmal weiß, dass sie beteiligt sein könnte – die Funktion sieht rein aus, aber ein tiefer Aufruf irgendwo in ihrem Body wirft, und die Exception räumt durch sie hindurch auf.

Das ist bequem für den Autor des werfenden Codes. Es ist teuer für alle anderen:

  • Leser (und Agenten) können einer Signatur nicht ansehen, ob eine Funktion an Fehlerpfaden beteiligt ist.
  • Refactoring wird nervös – einen Aufruf zwischen Funktionen zu verschieben kann ändern, welche Exceptions erreichbar sind.
  • Recovery-Code lebt weit weg von der Stelle, die weiß, was zu tun ist.

Zero zahlt die Kosten im Voraus – Annotationen an jeder fehlbaren Funktion und check an jedem fehlbaren Aufruf – um die Eigenschaft zu bekommen: „die Fehlerarten, an denen eine Funktion beteiligt ist, sind aus ihrer Signatur sichtbar“. Auf diese Eigenschaft können sich sowohl Menschen als auch Agenten verlassen.

Warum nicht einfach Result<T, E>?

Du könntest dasselbe mit einer choice ausdrücken – einem Result<T, E>-Typ mit ok- und err-Varianten. Zero gibt dir auch dieses Muster; es ist ein nützliches Werkzeug, wenn der Fehler Daten sind, die du inspizieren, speichern oder herumreichen willst.

Was raises/check hinzufügt, ist eine Syntax-Ebenen-Konvention für den häufigen Fall: „wenn das fehlschlägt, lass meine Funktion auf dieselbe Weise fehlschlagen“. Ohne sie wäre jeder Aufruf in ein match gewickelt, das den Fehler fast immer in das Result des Aufrufers umpackt. check ist die Abkürzung dafür, wobei der Compiler sicherstellt, dass die Propagierung typkonform ist.

Also:

  • Result<T, E> (eine choice) – wenn du Fehler als Wert inspizieren oder herumtragen willst.
  • raises + check – wenn du Fehler nur den Aufrufstack hoch propagieren willst.

Beides ist verfügbar; sie decken unterschiedliche Ergonomie-Bedürfnisse ab.

Mehrere Fehlertypen

Eine Funktion kann mehr als eine Art von Fehler werfen:

fun parse(input: String) -> i32 raises { Empty, Malformed } {
    if std.mem.len(input) == 0 {
        raise Empty
    }
    // ... Parser-Logik ...
    raise Malformed
}

Ein Aufrufer kann:

  • Mit check parse(input) propagieren, falls seine eigene Signatur sowohl Empty als auch Malformed (oder eine Obermenge) listet.
  • Einen oder beide Fehler explizit mit match oder try-artigen Konstrukten behandeln, die die Sprache dafür bereitstellt.

Die exakte Syntax für feingranulares Handling (auf bestimmte Fehlertypen matchen vs. andere propagieren) gehört zu den Oberflächen, die sich in pre-1.0 Zero noch ändern können. Der Vertrag – jeder Fehlertyp, den eine Funktion werfen kann, steht in ihrer Signatur – ist der stabile Teil.

Ein mentales Modell

Zeros Fehlersystem ist eine streng-ehrliche Version dessen, was jede imperative Sprache hat:

KonzeptTry/CatchZero
Funktion als fehlbar markierennichts (implizit)raises { ... }
Fehler werfenthrow eraise E
An Aufrufer propagierenblubbert unsichtbar nach obencheck call(...)
Lokal behandelntry { ... } catch(e) { ... }match auf ein zurückgegebenes Result oder eine typisierte Handling-Form

Der Verhaltensunterschied ist klein. Der Annotations-Unterschied ist groß – und mit Absicht. Effekte sind explizit. Fehler sind Effekte.

Stil-Hinweise

  • Verwende check großzügig. Es ist der richtige Default, wenn du auf dieser Ebene nichts Spezielles zu tun hast.
  • Vermeide bloßes raises an internen Helpern. Je enger das Fehler-Set, desto nützlicher die Signatur.
  • Paare fehlbare Operationen mit den Capabilities, die sie brauchen. Eine World-nutzende Funktion, die auf stdout schreibt, will fast immer raises, weil Schreibvorgänge fehlschlagen können.

Als Nächstes: JSON-Diagnostik

raises/check arbeitet Hand in Hand mit Zeros Compiler-Diagnostik – wenn du check gegen eine Funktion schreibst, die nicht die richtigen Fehler deklariert, sagt dir der Compiler in strukturierter Form genau, was nicht stimmt. Das nächste Dokument behandelt JSON-Diagnostik – den maschinenlesbaren Feed, den ein Agent liest, um Code zu reparieren.

Häufig gestellte Fragen

Was bedeutet raises in Zero?

raises in einer Funktionssignatur deklariert, dass die Funktion fehlschlagen kann. Ein bloßes raises erlaubt jeden Fehlertyp. Eine konkrete Form wie raises { InvalidInput } schränkt die Funktion auf das Fehlschlagen mit den gelisteten Fehlertypen ein. Aufrufer müssen die Möglichkeit des Fehlschlags bestätigen – entweder mit check oder mit einer anderen expliziten Handling-Form.

Was macht der check-Operator?

check expr wertet expr aus und propagiert den Fehler, falls einer entsteht, zum Aufrufer der aktuellen Funktion. Stell dir vor: „führ das aus, und wenn es fehlschlägt, lass meinen Aufrufer mit demselben Fehler fehlschlagen.“ Damit die Propagierung erlaubt ist, muss die raises-Klausel des Aufrufers selbst deklarieren, dass er einen kompatiblen Fehler werfen kann.

Wie wirft man in Zero einen Fehler?

Verwende raise FehlerName innerhalb einer Funktion, deren raises-Klausel diesen Fehler enthält. Beispiel: if ok == false { raise InvalidInput }. Die Funktion verlässt sich an dieser Stelle; der Fehler wird zum Ergebnis der Funktion, das der Aufrufer mit check weiterreichen kann.

Warum nutzt Zero kein try/catch?

Try/catch lässt Exceptions stillschweigend durch Funktionen wandern, die nichts davon wissen. Zeros Design ist, dass jede Funktionssignatur die Fehlerarten anerkennt, an denen sie beteiligt ist. Es gibt keinen versteckten Kontrollfluss – wenn eine Funktion fehlschlagen kann, sagt das ihre Signatur, und jeder Aufrufer muss das explizit mit check bestätigen.

Kann eine Funktion in Zero mehrere Fehlertypen werfen?

Ja – liste sie in der raises { ... }-Klausel auf, kommagetrennt (oder gemäß der aktuellen Zero-Syntax für Fehler-Sets). Die Menge möglicher Fehler ist Teil des Vertrags der Funktion, genau wie ihre Parameter- und Rückgabetypen. Aufrufer können auf den geworfenen Fehler pattern-matchen oder ihn einfach mit check propagieren.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S