11 KiB
18. Result, erreurs et exceptions
18.1 Hiérarchie Core — V1 REQUIS — FIGÉ
La hiérarchie d'erreurs V1 est :
Object
└── Error
├── ResultError
└── Exception
Les trois classes racines sont abstraites et ne sont donc jamais instanciées directement.
Error est la racine abstraite commune des erreurs Saselang. Elle ne détermine pas à elle seule le mécanisme de propagation.
ResultError représente exclusivement la famille des erreurs transportables par Result<T,E>.
Exception représente exclusivement la famille des erreurs propagées par throw / throws / try / catch / finally.
ResultError et Exception sont des branches sœurs. Une classe utilisateur destinée à un Result hérite directement ou indirectement de ResultError. Une exception utilisateur hérite directement ou indirectement de Exception.
Exemple :
public final class ParseError extends ResultError {
}
public final class FileNotFoundException extends Exception {
}
L'interface Throwable ne fait pas partie de Saselang V1. Elle pourra être réintroduite ultérieurement si un besoin réel apparaît sans modifier la règle fondamentale : Result transporte des ResultError, tandis que throw transporte des Exception.
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 ou une Exception
n'est pas automatiquement propagée
i18nMessage
clé et paramètres de localisation
ne contient pas un tableau de traductions
cause est de type Option<Error> et non Nullable<Error> : l'absence de cause est une absence sémantique. Une cause de type statique Error ne peut pas être passée directement à throw; il faut d'abord prouver par is qu'elle est une Exception, ou l'encapsuler explicitement dans une nouvelle exception.
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, SDKs ou couches de logging/diagnostic lorsqu'elles sont pertinentes.
Le type exact I18nMessage, sa clé et la représentation de ses paramètres seront finalisés avec Core/SDK et le système de formatting/i18n.
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. Exception n'a pas de code obligatoire.
18.4 Exception et stack trace — V1 REQUIS — FIGÉ EN PRINCIPE
Exception reste généraliste. Elle peut recevoir des informations de stack trace liées à sa propagation.
La création d'un objet Exception ne doit pas obligatoirement capturer immédiatement une stack trace. Le contexte de propagation peut être attaché ou capturé lors de :
throw exception;
La représentation exacte (StackTrace, StackFrame, capture lazy ou autre optimisation) relève de Core/runtime. Cette information reste diagnostique et n'intervient pas dans la sélection des catch.
Error et ResultError n'ont 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,FileNotFoundException>
La forme courte :
Result<T>
est équivalente à :
Result<T,ResultError>
Result<Void,E> et Result<Void> sont autorisés pour représenter un succès sans payload utile. 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);
sont explicites.
Result suit les règles générales des enums algébriques. Son inspection et l'extraction sûre de ses payloads utilisent match avec bindings typés. V1 n'introduit ni unwrap, ni opérateur ?, ni extraction implicite d'une variante.
Des helpers Core futurs tels que :
result::expectOk(...)
result::expectErr(...)
peuvent être étudiés si un besoin réel apparaît. Ils resteraient une API de Result, pas une syntaxe du langage, et leur comportement d'échec devrait être explicitement spécifié.
18.6 Séparation ResultError / Exception — V1 REQUIS — FIGÉ
Aucune conversion automatique n'existe entre les deux branches :
ResultError -> Exception
Exception -> ResultError
La traduction d'un mécanisme vers l'autre se fait par encapsulation explicite dans une nouvelle erreur de la branche cible, éventuellement en conservant l'erreur source dans cause.
Exemples conceptuels :
throw ParseException(..., Option::Some(parseError));
ou :
catch (FileNotFoundException error) {
return Result::Err(FileResultError(..., Option::Some(error)));
}
Un simple cast ne transforme pas un ResultError en Exception ni l'inverse.
18.7 throws — V1 REQUIS — FIGÉ
throws est interdit sur une func / method / clsmethod à retour direct. Il n'est permis que si le retour est Result<T> ou Result<T,E>.
T + throws -> interdit
Result<T,E> -> valide sans throws
Result<T,E> + throws -> valide
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 clause :
throws IOException
peut donc couvrir FileNotFoundException si cette dernière hérite de IOException.
Une même clause throws ne doit pas contenir de types redondants lorsqu'un type déclaré couvre déjà entièrement un autre type de la liste.
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 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. Lorsque plusieurs interfaces portent la même signature, l'implémentation doit satisfaire simultanément tous leurs contrats throws. 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(...); // valide
throw ParseError(...); // erreur
throw Error(...); // erreur
Les racines abstraites Error, ResultError et Exception ne sont jamais instanciables directement.
La forme de repropagation reste explicite :
catch (IOException error) {
throw error;
}
Il n'existe pas de forme spéciale throw; en V1.
18.10 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 (ParentException error) {
...
} finally {
...
}
finally est facultatif et ne peut apparaître qu'après au moins un catch.
Chaque catch contient exactement un type explicite d'Exception ou descendant. Il n'existe pas de multi-catch A | B en V1 : des blocs catch distincts sont utilisés.
Les catch sont évalués dans l'ordre source. Le chevauchement par héritage est normal et utile : les cas spécifiques peuvent précéder un fallback de famille générale.
Un catch entièrement couvert par un catch précédent est une erreur de compilation, pas un simple warning.
Exemple valide :
catch (FileNotFoundException error) {
...
} catch (IOException error) {
...
}
Exemple invalide si FileNotFoundException extends IOException :
catch (IOException error) {
...
} catch (FileNotFoundException error) {
...
}
Un ensemble de catch n'a pas à être exhaustif. Toute Exception non capturée continue à se propager et doit être couverte par le contrat throws de la callable englobante.
Les variables de catch suivent les règles normales de scope et de non-shadowing. Des catch distincts peuvent réutiliser le même nom parce que leurs scopes sont disjoints.
18.11 finally non-escaping — V1 REQUIS — FIGÉ
finally exécute un bloc commun avant de quitter la construction try/catch, qu'il y ait :
fin normale du try
fin normale d'un catch
return traversant la construction
throw propagé
break / continue traversant la construction
Exception non capturée par les catch
finally ne doit jamais remplacer une sortie déjà en cours. Les sorties structurées suivantes y sont interdites :
return
throw
break
continue
emit
Aucune Exception non capturée ne peut sortir indirectement d'un finally. Toute callable appelée depuis finally dont le contrat comporte throws doit avoir ses exceptions entièrement gérées à l'intérieur du finally.
Une faute runtime non récupérable reste distincte de ce contrat d'exception.
18.12 Faute runtime / panic — V1 REQUIS — À FINALISER
Les violations de contrat du langage telles que :
index hors limites
division entière par zéro
overflow checked
représentation mémoire invalide
borne dynamique invalide lors d'une construction directe lorsque la règle du type le définit
ne sont pas nécessairement des ResultError ou des Exception récupérables.
Le modèle exact de faute runtime/fatal error/panic et son interaction avec cleanup/destruction doit être défini avant la finalisation V1.