Quand false ne veut rien dire
L'impact de true/false sur la simplicité des contrats de méthode

Directeur Craft / Devops chez Sopra Steria Next
Le bool qui fâche
Les méthodes ne doivent jamais (ou presque) retourner bool
Quand une fonction peut échouer pour une ou plusieurs causes, retourner bool (true/false) fait perdre l'information la plus utile : le “pourquoi”.
L'appelant reçoit
falseet ne sait pas quoi faire (retry ? message utilisateur ? mapping HTTP ? compensation ?)."Mais on log l'erreur" disait Peppa Pig! Et bah ça ne règle pas le problème : les logs ne sont pas une API (ils peuvent être absents, filtrés, non corrélés, non accessibles, etc.).
L'objectif : remonter une cause exploitable (code d'erreur, type d'erreur, contexte), pas juste “KO”.
Focus C : historique + patterns
Historique : le C n'avait pas de bool (avant C99)
Historiquement, le C (C89/C90) n'avait pas de type booléen natif : on utilisait des int avec la convention :
0= fauxnon-zéro = vrai
Depuis C99, on a <stdbool.h> et bool, mais la culture des API C est restée :
0 = succès, non-zéro = erreur (souvent négatif), ou \>= 0 = succès, -1 = erreur + errno. Sans oublier les pièges que le nouveau type bool a apportés.
L’anti-pattern : bool
#include <stdbool.h>
bool save_user(struct User u) {
if (!validate(u)) { log_error("invalid user"); return false; }
if (!db_connect()) { log_error("db down"); return false; }
if (!write_row(u)) { log_error("io error"); return false; }
return true;
}
if (!save_user(u)) {
// 🤷 L'appelant ne sait pas quoi faire.
// Impossible de distinguer invalid vs conflit vs IO vs timeout...
}
Pourquoi c'est mauvais
L'appelant ne peut pas adapter son comportement.
Les tests ne peuvent pas affirmer précisément la cause.
On finit par parser des logs / messages → fragile et non contractuel.
En plus, c'est un anti-pattern avéré (cf. Martin Fowler, Eric Evans, etc.)
Bon pattern en C (pour dépoussiérer la mémoire des plus vieux)
Convention simple
0: succès (OK)< 0: erreurs (codées)
// errors.h
#pragma once
#define OK 0
#define ERR_INVALID -1
#define ERR_IO -2
#define ERR_CONFLICT -3
#define ERR_TIMEOUT -4
// user_service.c
#include "errors.h"
int save_user(const struct User* u) {
if (!validate_user(u)) return ERR_INVALID;
if (email_exists(u->email)) return ERR_CONFLICT;
if (!db_connect()) return ERR_IO;
if (!write_row(u)) return ERR_IO;
return OK;
}
Usage simpliste :
int rc = save_user(&u);
if (rc != OK) {
switch (rc) {
case ERR_INVALID: /* message / 400 */ break;
case ERR_CONFLICT: /* 409 */ break;
case ERR_TIMEOUT: /* retry */ break;
default: /* 500 */ break;
}
}
✅ L'appelant sait pourquoi et peut décider quoi faire.
Variante C : “bitmask” (octets 00000001, 00000010, …)
Quand il peut y avoir plusieurs erreurs en même temps (ou des statuts cumulables), on peut utiliser des flags. Toujours d'actualité même dans GO.
Exemple en octet (uint8_t) :
#include <stdint.h>
#define E_NONE 0x00 // 00000000
#define E_INVALID 0x01 // 00000001
#define E_IO 0x02 // 00000010
#define E_CONFLICT 0x04 // 00000100
#define E_TIMEOUT 0x08 // 00001000
uint8_t validate_and_save(const struct User* u) {
uint8_t err = E_NONE;
if (!validate_user(u)) err |= E_INVALID;
if (email_exists(u->email)) err |= E_CONFLICT;
// On peut choisir d'arrêter tôt, ou continuer pour collecter plusieurs erreurs
if (err != E_NONE) return err;
if (!db_connect()) err |= E_IO;
if (!write_row(u)) err |= E_IO;
return err;
}
Usage :
uint8_t err = validate_and_save(&u);
if (err & E_INVALID) { /* ... */ }
if (err & E_CONFLICT) { /* ... */ }
if (err & E_IO) { /* ... */ }
✅ Intéressant si :
plusieurs états peuvent coexister,
on veut agréger des validations (ex : formulaire).
⚠️ Moins adapté si :
on a besoin d'un seul code précis + contexte riche,
on veut une hiérarchie d'erreurs.
Autres styles (au choix)… tant qu'on évite l'anti-pattern
Il n'y a pas “un” style universel : l'essentiel est d'éviter bool ambigu.
Result<T>
On peut se servir du pattern Result :
Success / Failure
- un code d'erreur (enum)
- message + détails
Il existe une bibliothèque populaire, Ardalis.Result, qui fournit un
Resultprêt à l'emploi.
Exemple minimal :
public enum SaveUserError { InvalidInput, Conflict, IoFailure, Timeout }
public sealed record Result<TError>(bool IsSuccess, TError? Error, string? Message)
{
public static Result<TError> Ok() => new(true, default, null);
public static Result<TError> Fail(TError error, string? message = null) => new(false, error, message);
}
✅ L'appelant peut faire un mapping propre (HTTP 400/409/500…), afficher un message UX, décider de retry, etc.
Exception métier / technique (Approche DDD)
throw new InvalidEmailException(...)
throw new InvalidArgumentException(...)
✅ L'appelant peut distinguer “expected business error” vs “technical error”.
Enum ou constante
public enum SaveUserError
{
None = 0, // Succès (équivalent de OK)
InvalidInput = 1,
Conflict = 2,
IoFailure = 3,
Timeout = 4
}
// OU
public static class ErrorCodes
{
public const string USER_INVALID = "USER_INVALID";
public const string USER_CONFLICT = "USER_CONFLICT";
}
Conclusion
Règles simples
✅
boolà éviter voire à bannir✅ code d'erreur / Result / error / enum / bitmask / ...
✅ Les logs aident le diagnostic, mais ne remplacent pas un contrat de retour
Choisissez le style qui vous convient mais ne choisissez pas l'anti-pattern !



