13 KiB
18. Result, erreurs, exceptions et faults
18.1 Hiérarchie Core — V1 REQUIS — FIGÉ
La hiérarchie V1 est :
Object
└── Error
├── ResultError
├── Exception
└── Fault
Les quatre classes racines sont abstraites et ne sont jamais instanciées directement.
Error est la racine commune. Elle ne choisit pas à elle seule le mécanisme de propagation.
ResultError
échec transporté explicitement comme une valeur dans Result<T,E>
Exception
échec checked propagé par throw / throws
Fault
échec unchecked, capturable, pouvant être documenté par faults
Les trois branches sont sœurs. Aucune n'est implicitement convertible vers une autre.
Exemple :
public final class ParseError extends ResultError {
}
public final class FileNotFoundException extends Exception {
}
public final class IndexOutOfBoundsFault extends Fault {
}
Il n'existe pas d'interface Throwable en V1.
18.2 Informations communes de Error — V1 REQUIS — FIGÉ EN PRINCIPE
Error reste volontairement généraliste et minimal. Il porte conceptuellement :
message: String
cause: Option<Error>
i18nMessage: Option<I18nMessage>
Rôles :
message
message humain canonique / fallback
cause
erreur causale purement informative
peut contenir un ResultError, une Exception ou un Fault
n'est pas automatiquement propagée
i18nMessage
clé et paramètres de localisation
ne contient pas un tableau de traductions
cause n'accorde aucun droit de propagation. Une valeur statiquement typée Error ne peut être utilisée ni avec throw ni avec fault sans preuve préalable de sa branche exacte.
message, cause et i18nMessage sont initialisés lors de la construction et ne sont pas publiquement mutables.
Error ne porte pas de propriétés universelles supplémentaires telles que timestamp, thread id, process id, host, severity ou code plateforme. Ces informations appartiennent aux sous-classes, au SDK ou aux couches de logging/diagnostic lorsqu'elles sont pertinentes.
18.3 ResultError et code machine-readable — V1 REQUIS — FIGÉ EN PRINCIPE
ResultError ajoute conceptuellement un code stable, non localisé et exploitable par le programme :
code: ResultErrorCode
Le rôle de ResultErrorCode est distinct de message :
code -> stable, machine-readable, non localisé
message -> humain, canonique / fallback
i18nMessage -> localisation externe
La représentation exacte de ResultErrorCode sera finalisée avec Core/SDK.
Ni Exception ni Fault n'ont de code universel obligatoire ; leurs sous-types peuvent naturellement en définir lorsqu'il est utile.
18.4 Diagnostic de Exception et Fault — V1 REQUIS — FIGÉ EN PRINCIPE
Exception et Fault peuvent recevoir des informations de stack trace liées à leur propagation runtime.
La création de l'objet ne doit pas obligatoirement capturer immédiatement une stack trace. Le contexte peut être attaché ou capturé lors de :
throw exception;
fault someFault;
ou lorsqu'un Fault est produit intrinsèquement par le runtime/Core.
La représentation exacte (StackTrace, StackFrame, capture lazy ou autre optimisation) relève du Core/runtime. Cette information reste diagnostique et n'intervient pas dans la sélection des catch.
ResultError n'a pas de stack trace automatique obligatoire.
18.5 Result<T,E> — V1 REQUIS — FIGÉ
Result<T,E> est un type Core algébrique avec exactement deux variantes conceptuelles :
Result::Ok(T)
Result::Err(E)
Le paramètre d'erreur est contraint :
E doit être ResultError ou un descendant de ResultError
Sont valides :
Result<Data,ResultError>
Result<Data,ParseError>
Result<Void,ResultError>
Sont invalides :
Result<Data,Error>
Result<Data,Exception>
Result<Data,Fault>
Result<Data,FileNotFoundException>
La forme courte :
Result<T>
est équivalente à :
Result<T,ResultError>
Result<Void,E> et Result<Void> sont autorisés. Option<Void> reste interdit.
Il n'existe aucune conversion implicite de T vers Result<T,E> ni de E vers Result<T,E> :
return Result::Ok(value);
return Result::Err(error);
restent explicites.
Result suit les règles générales des enums algébriques. V1 n'introduit ni unwrap, ni opérateur ?, ni extraction implicite d'une variante.
18.6 Choix du mécanisme d'échec — V1 REQUIS — FIGÉ EN PRINCIPE
Les trois branches ne sont pas trois orthographes du même concept.
ResultError / Result
l'échec est une donnée normale du résultat et l'appelant doit l'inspecter explicitement
Exception
l'échec appartient au contrat checked de la callable et doit être capturé ou propagé par throws
Fault
l'échec est unchecked ; l'appelant peut le capturer mais n'y est pas obligé
Une API directe n'est donc plus obligée de retourner Result uniquement parce qu'elle peut échouer.
Exemple :
method elementAt(uint64 index) -> T
faults IndexOutOfBoundsFault
peut retourner directement T.
Inversement, lorsqu'une absence ou un échec constitue une donnée normale du domaine, Option ou Result reste préférable :
map::get(key) -> Option<V>
parseExternalInput(...) -> Result<Value, ParseError>
Le Core ne doit pas créer mécaniquement un couple op() / tryOp() uniquement pour offrir d'un côté une valeur directe et de l'autre un Result. Deux opérations coexistantes doivent avoir des sémantiques réellement distinctes.
Aucune conversion automatique n'existe entre ResultError, Exception et Fault. Une traduction entre branches se fait par encapsulation explicite, éventuellement via cause.
18.7 throws — V1 REQUIS — FIGÉ
throws est indépendant du type de retour.
Sont valides :
T
T throws SomeException
Result<T,E>
Result<T,E> throws SomeException
Void throws SomeException
Tout type déclaré dans throws doit être Exception ou un descendant de Exception.
Une callable doit déclarer toute famille d'exception susceptible d'atteindre sa frontière sans être gérée localement. Cette obligation est transitive : une exception déclarée par une callable appelée doit être soit capturée localement, soit couverte par le throws de l'appelant.
Un type déclaré dans throws couvre ses descendants. Une même clause ne doit pas contenir de types redondants lorsqu'un type déclaré couvre déjà entièrement un autre type de la liste.
Fault, Error et ResultError sont interdits dans throws.
18.8 throws dans les contrats, interfaces et overrides — V1 REQUIS — FIGÉ
throws ne fait pas partie de la signature d'overload. Il fait partie du contrat checked de la callable.
Un override ou une implémentation peut :
supprimer entièrement des exceptions déclarées
restreindre une famille à une ou plusieurs sous-familles compatibles
gérer localement tout ou partie des exceptions du contrat parent
Il ne peut jamais ajouter une exception non couverte par le contrat parent ni élargir une famille déclarée.
La même règle s'applique aux contrats d'interface. Une implémentation sans exception sortante est toujours compatible avec un contrat qui en autorise.
18.9 throw — V1 REQUIS — FIGÉ
throw ne peut lancer qu'une valeur dont le type statique est Exception ou un descendant de Exception.
throw FileNotFoundException(...); // OK
throw ParseError(...); // ERROR
throw IndexOutOfBoundsFault(...); // ERROR
throw Error(...); // ERROR
La repropagation reste explicite :
catch (IOException error) {
throw error;
}
Il n'existe pas de forme spéciale throw; en V1.
18.10 fault et faults — V1 REQUIS — FIGÉ EN PRINCIPE
fault produit explicitement un Fault :
fault InvalidStateFault(...);
Le type statique de la valeur doit être Fault ou un descendant de Fault.
fault InvalidStateFault(...); // OK
fault FileNotFoundException(...); // ERROR
fault ParseError(...); // ERROR
Une callable peut documenter des faults significatifs directement dans sa signature :
method elementAt(uint64 index) -> T
faults IndexOutOfBoundsFault
Plusieurs familles peuvent être listées explicitement :
faults FirstFault, SecondFault
Chaque type listé doit être Fault ou un descendant de Fault; une liste ne doit pas contenir de famille redondante déjà couverte par un parent déclaré.
La clause faults est optionnelle et non exhaustive.
Elle signifie que les faults listés font explicitement partie du contrat/documentation utile de l'API. Elle ne signifie pas qu'aucun autre fault runtime ne peut survenir.
Contrairement à throws :
aucun catch n'est obligatoire
aucune propagation de la clause faults n'est obligatoire
un appelant n'a pas à recopier les faults d'une callable appelée
Exemple valide :
func outer() -> Void {
inner(); // inner peut déclarer faults SomeFault
return Void;
}
outer peut, mais n'est pas obligé de déclarer à son tour :
faults SomeFault
faults ne fait pas partie de la signature d'overload et n'impose pas les restrictions de covariance contractuelle de throws. Un override peut documenter un ensemble différent de faults, puisque l'absence d'un fault dans la clause n'est jamais une garantie statique d'impossibilité.
Une API publique qui produit explicitement un fault important devrait normalement le documenter avec faults; un outil/linter peut signaler les omissions sans en faire une erreur de compilation du langage.
18.11 try / catch / finally — V1 REQUIS — FIGÉ
Un try doit être suivi d'au moins un catch.
Sont interdits :
try { ... }
try { ... } finally { ... }
finally { ... }
La forme générale est :
try {
...
} catch (SpecificException error) {
...
} catch (SomeFault faultValue) {
...
} finally {
...
}
finally est facultatif et ne peut apparaître qu'après au moins un catch.
Le type explicite d'un catch doit être :
Exception ou un descendant de Exception
Fault ou un descendant de Fault
Sont donc interdits :
catch (Error error)
catch (ResultError error)
Il n'existe pas de multi-catch A | B en V1.
Les catch sont évalués dans l'ordre source. Un catch entièrement couvert par un précédent dans la même branche d'héritage est une erreur de compilation.
Une Exception non capturée continue à se propager et doit être couverte par throws.
Un Fault non capturé continue à se propager de façon unchecked ; aucune clause faults n'est exigée sur les callables traversées.
Les variables de catch suivent les règles normales de scope et de non-shadowing.
18.12 finally non-escaping — V1 REQUIS — FIGÉ EN PRINCIPE
finally exécute un bloc commun avant de quitter la construction try/catch, notamment sur :
fin normale du try
fin normale d'un catch
return traversant la construction
throw propagé
Fault propagé
break / continue traversant la construction
finally ne doit pas remplacer volontairement une sortie déjà en cours. Les sorties structurées explicites suivantes y sont interdites :
return
throw
fault
break
continue
emit
Toute Exception produite indirectement depuis finally doit être gérée localement selon les règles checked de throws.
Un Fault dynamique peut néanmoins survenir dans le code exécuté par finally, puisqu'il est unchecked. L'interaction exacte entre un tel fault, l'unwind, les destructeurs et une sortie déjà en cours reste à fermer avec le modèle mémoire/runtime.
18.13 Sémantique runtime des Fault — V1 REQUIS — FIGÉ EN PRINCIPE
Les Fault couvrent notamment les échecs unchecked et violations runtime tels que :
index dynamique hors limites
division entière dynamique par zéro
overflow checked
iterator invalidé
clé absente lors d'un accès strict map[key]
doublon lors d'un insert strict
conversion explicite directe dont les préconditions runtime ne sont pas satisfaites
représentation/encodage invalide lors d'une construction directe validée
Lorsqu'une violation d'une précondition intrinsèque est démontrable statiquement, le compilateur doit la diagnostiquer plutôt que générer un programme condamné à fauter.
Exemple :
StaticArray<int32,3> values = [1, 2, 3];
int32 value = values[5]; // ERROR compilation
Cette règle ne rend évidemment pas illégal un statement fault explicite : fault SomeFault(...) est un mécanisme volontaire du programme et peut être utilisé pour implémenter une API.
Lorsqu'un Fault ne peut être déterminé qu'au runtime :
capturé par catch
-> le catch correspondant s'exécute
non capturé
-> le fault remonte sans contrat checked
-> à la frontière de l'unité d'exécution, il provoque un échec runtime diagnostiqué
La définition exacte de « l'unité d'exécution » (processus, thread, task, isolate éventuel) et l'ordre final cleanup/destruction lors d'un fault seront fermés avec les chapitres mémoire et concurrence.