UC-11 · Un Result est testé ou renvoyé, jamais ignoré
Signal
- Un appel qui renvoie un
Result, écrit comme une instruction :wallet.block(reference, at);,await this.#ledger.post(entry);. - Un
Resultrangé dans une variable que rien ne lit. - Une promesse de
Resultjamais attendue. - Un
Resultlu sans que sonoksoit testé :return ok(posted).
Pourquoi
Un refus est une valeur, et TypeScript n’oblige pas à lire une valeur. Une exception oubliée remonte jusqu’à l’appelant. Un Result oublié ne fait aucun bruit.
L’aggregate a refusé, et il n’a pas changé. Le use case enregistre quand même, puis renvoie ok : le client croit que l’opération a eu lieu. Rien n’a échoué, rien n’est annulé, la réponse est simplement fausse.
Exceptions
Aucune. Un refus qu’on choisit vraiment d’ignorer se dit par un waiver, avec sa raison.
Correction
Tester ok juste après l’appel. Renvoyer le refus tel quel, ou le traduire en un refus nommé pour l’appelant. Une suite d’appels qui peuvent refuser se teste un par un, ou se combine (combine, andThen).
Adaptation
Notre choix : @alveolus/lint-config le vérifie avec ESLint. alveolus/uc-11-must-use-result lit le type de chaque instruction, et signale celle qui jette un Result : une affectation le garde, elle passe. alveolus/uc-11-no-unused-vars signale la variable locale qui tient un Result et que rien ne lit, et alveolus/uc-11-no-floating-promises la promesse de Result oubliée. Les trois ne regardent que les Result : un paramètre inutilisé ou une autre promesse oubliée ne relève pas de cette règle. Un Result lu sans être testé reste à la review : le décider sans faux positif demanderait de suivre chaque valeur.
Le tsconfig active aussi noUnusedLocals, pour qu’une variable jamais lue casse la compilation. À l’exécution, un run imbriqué qui refuse empêche la transaction de valider (REP-13), même quand l’appelant oublie le refus.
Voir aussi
UC-07 fait de l’absence un refus nommé. REP-03 donne à chaque issue d’un port son canal : un refus en Result, une absence en undefined, une panne en exception. REP-13 annule une transaction dont un run imbriqué a refusé.
Exemple
// Avoid: the refusal of the wallet is dropped, and the use case answers ok
const wallet = await this.#wallets.findById(walletId);
if (!wallet) {
return err(new WalletNotFound(walletId));
}
wallet.block(reference, this.#clock.now());
return this.#wallets.save(wallet);// Preferred: the use case returns the refusal of the wallet
const wallet = await this.#wallets.findById(walletId);
if (!wallet) {
return err(new WalletNotFound(walletId));
}
const blocked = wallet.block(reference, this.#clock.now());
if (!blocked.ok) {
return blocked;
}
return this.#wallets.save(wallet);Dans le premier bloc, un wallet déjà fermé refuse le blocage. Le use case l’enregistre tel quel et renvoie ok : le client croit son wallet bloqué.