Le contexte
L'échec en Zero n'est pas un flux de contrôle parallèle séparé. Il fait partie du flux de contrôle régulier, déclaré sur la signature d'une fonction et reconnu à chaque site d'appel. Deux pièces font fonctionner ça :
raisesdans la signature de la fonction — « cette fonction peut échouer ».checkau site d'appel — « si ça échoue, fais échouer ma fonction avec la même erreur ».
La combinaison suffit à exprimer ce que la plupart des langages traitent avec try/catch ou les types Result.
Déclarer une fonction faillible
Ajoutez raises après le type de retour :
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
Deux façons de lire la signature :
- «
validateretourne uni32ou lèveInvalidInput. » - « L'ensemble des issues possibles est {
i32,InvalidInput}. »
Les deux lectures sont correctes. Le compilateur suit les deux possibilités et exige des appelants qu'ils fassent quelque chose d'explicite pour chacune.
Vous pouvez aussi écrire une clause raises nue :
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
raises (sans liste d'erreurs) signifie « cette fonction peut échouer avec n'importe quelle erreur ». Sur main, c'est la forme conventionnelle — le programme peut se terminer avec un statut non nul si quelque chose tourne mal, et le runtime se charge de faire remonter l'erreur.
Pour les fonctions plus profondes dans la pile d'appels, préférez la forme explicite raises { ErrorA, ErrorB } pour que les modes d'échec soient documentés à chaque niveau.
Lever une erreur
À l'intérieur d'une fonction faillible, raise sort de la fonction avec l'erreur donnée :
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raise InvalidInput produit une erreur InvalidInput à cette ligne. La fonction ne continue pas au-delà ; le contrôle revient à l'appelant, qui voit l'erreur au lieu d'un i32. La clause raises liste les seuls types d'erreurs que cette fonction est autorisée à lever ; lever quelque chose qui n'est pas dans la liste est une erreur de compilation.
Propager une erreur avec check
Un appelant qui invoque une fonction faillible doit reconnaître l'échec possible. La reconnaissance la plus courante est check :
fun run() -> Void raises { InvalidInput } {
check validate(true)
}
check validate(true) fait deux choses :
- Appelle
validate(true). - Si
validatea levé une erreur, la propage vers le haut —runlève la même erreur à son appelant.
Pour que la propagation soit autorisée, run doit déclarer qu'il peut lever InvalidInput (ou quelque chose de compatible) dans sa propre clause raises. Le compilateur vérifie ça. Si la signature de run disait raises { OtherError }, la propagation ne compilerait pas parce que InvalidInput n'est pas dans l'ensemble.
Un exemple complet tiré des échantillons officiels du langage — cliquez sur Run pour voir la propagation réussir :
Le type d'erreur voyage avec la signature de la fonction tout le long de la pile d'appels. Le raises nu de main accepte tout ce que run pourrait lever, donc la propagation atterrit sans encombre.
Pourquoi pas try/catch ?
La discipline de conception derrière raises/check, c'est que l'échec n'est jamais invisible à un site d'appel. Dans un langage à try/catch, une exception peut traverser silencieusement une fonction qui n'a même pas conscience d'être impliquée — la fonction paraît pure, mais un appel profond quelque part dans son corps lève une exception qui se déroule à travers elle.
C'est pratique pour l'auteur du code qui lève. C'est coûteux pour tout le monde :
- Les lecteurs (et les agents) ne peuvent pas dire depuis une signature si une fonction participe à des chemins d'échec.
- La refactorisation devient stressante — déplacer un appel entre fonctions peut changer quelles exceptions sont accessibles.
- Le code de récupération vit loin de l'endroit qui sait quoi faire.
Zero paie le coût d'entrée — des annotations sur chaque fonction faillible et un check à chaque appel faillible — pour obtenir la propriété « les modes d'échec auxquels une fonction participe sont visibles depuis sa signature ». C'est une propriété sur laquelle les humains comme les agents peuvent compter.
Pourquoi pas juste Result<T, E> ?
Vous pourriez exprimer la même chose avec un choice — un type Result<T, E> avec des variantes ok et err. Zero vous donne aussi ce motif ; c'est un outil utile quand l'échec est une donnée que vous voulez inspecter, stocker ou faire circuler.
Ce que raises/check ajoute, c'est une convention au niveau syntaxique pour le cas courant : « si ça échoue, fais échouer ma fonction de la même façon ». Sans ça, chaque appel serait emballé dans un match qui re-paquette presque toujours l'erreur dans le propre Result de l'appelant. check est le raccourci pour ça, avec le compilateur qui s'assure que la propagation est bien typée.
Donc :
Result<T, E>(un choice) — quand vous voulez inspecter ou porter l'échec comme une valeur.raises+check— quand vous voulez juste propager l'échec vers le haut de la pile d'appels.
Les deux sont disponibles ; ils couvrent des besoins ergonomiques différents.
Plusieurs types d'erreurs
Une fonction peut lever plus d'un type d'erreur :
fun parse(input: String) -> i32 raises { Empty, Malformed } {
if std.mem.len(input) == 0 {
raise Empty
}
// ... logique de parsing ...
raise Malformed
}
Un appelant peut :
- Propager avec
check parse(input)si sa propre signature liste à la foisEmptyetMalformed(ou un sur-ensemble). - Gérer une ou les deux erreurs explicitement avec
matchou des constructions façontryque le langage expose à cet effet.
La syntaxe exacte pour la gestion fine (filtrer sur des types d'erreurs précis vs. propager les autres) est l'une des surfaces qui pourraient bouger en Zero pré-1.0. Le contrat — chaque type d'erreur qu'une fonction peut lever figure dans sa signature — est la partie stable.
Un modèle mental
Le système d'échec de Zero est une version strict-honnête de ce que tous les langages impératifs ont :
| Concept | Try/Catch | Zero |
|---|---|---|
| Marquer une fonction comme faillible | rien (implicite) | raises { ... } |
| Lever une erreur | throw e | raise E |
| Propager à l'appelant | remonte invisiblement | check call(...) |
| Traiter localement | try { ... } catch(e) { ... } | match sur un Result retourné, ou une forme de gestion typée |
La différence comportementale est petite. La différence annotationnelle est grande — et c'est voulu. Les effets sont explicites. Les échecs sont des effets.
Notes de style
- Utilisez
checkgénéreusement. C'est le bon défaut quand vous n'avez rien de spécifique à faire à cette couche. - Évitez un
raisesnu sur les fonctions utilitaires internes. Plus l'ensemble d'erreurs est restreint, plus la signature est utile. - Couplez les opérations faillibles aux capacités dont elles ont besoin. Une fonction qui utilise
Worldpour écrire sur stdout veut presque toujoursraisesparce que les écritures peuvent échouer.
La suite : diagnostics JSON
raises/check fonctionne main dans la main avec les diagnostics du compilateur Zero — quand vous écrivez check contre une fonction qui ne déclare pas les bonnes erreurs, le compilateur vous dit exactement ce qui cloche sous une forme structurée. Le prochain document couvre les diagnostics JSON — le flux lisible par machine qu'un agent lit pour réparer le code.
Questions fréquentes
Que signifie raises en Zero ?
raises en Zero ?raises sur la signature d'une fonction déclare que la fonction peut échouer. Un raises nu autorise n'importe quel type d'erreur. Une forme précise comme raises { InvalidInput } restreint la fonction à n'échouer qu'avec les types d'erreurs listés. Les appelants doivent reconnaître la possibilité d'échec — soit avec check, soit avec une autre forme de gestion explicite.
Que fait l'opérateur check ?
check ?check expr évalue expr, et si elle produit une erreur, propage cette erreur à l'appelant de la fonction courante. Pensez-y comme « exécute ça, et si ça échoue, fais échouer mon appelant avec la même erreur. » Pour que la clause raises de l'appelant autorise la propagation, l'appelant doit lui-même déclarer qu'il peut lever une erreur compatible.
Comment lever une erreur en Zero ?
Utilisez raise ErrorName à l'intérieur d'une fonction dont la clause raises inclut cette erreur. Exemple : if ok == false { raise InvalidInput }. La fonction sort à ce point ; l'erreur devient le résultat de la fonction, que l'appelant peut traiter avec check.
Pourquoi Zero n'utilise-t-il pas try/catch ?
Try/catch laisse les exceptions traverser silencieusement des fonctions qui n'en ont pas conscience. La conception de Zero veut que la signature de chaque fonction reconnaisse les modes d'échec auxquels elle participe. Pas de flux de contrôle caché — si une fonction peut échouer, sa signature le dit, et chaque appelant doit le reconnaître explicitement avec check.
Une fonction peut-elle lever plusieurs types d'erreurs en Zero ?
Oui — listez-les dans la clause raises { ... }, séparés par des virgules (ou en suivant la syntaxe Zero actuelle pour les ensembles d'erreurs). L'ensemble des erreurs possibles fait partie du contrat de la fonction, tout comme ses paramètres et son type de retour. Les appelants peuvent filtrer par motifs sur quelle erreur a été levée ou simplement la propager avec check.