backend / go
Gestion d'erreurs idiomatique
Explication
Ce que vous allez apprendre
- Comprendre pourquoi
errorest une simple interface, pas un mécanisme spécial du langage - Créer des erreurs sentinelles avec
errors.Newet les comparer avecerrors.Is - Créer un type d'erreur custom et l'extraire avec
errors.As - Envelopper une erreur avec
fmt.Errorf("...: %w", err)sans perdre son contexte - Regrouper plusieurs erreurs avec
errors.Join(Go 1.20+)
Dans quel contexte ?
Une API doit distinguer trois situations distinctes quand un client demande un utilisateur par son ID : l'ID est invalide (erreur 400), l'utilisateur n'existe pas (erreur 404), ou une panne de base de données survient (erreur 500). Sans un système d'erreurs structuré, ces trois cas se retrouveraient tous mélangés dans un simple message texte, impossible à distinguer proprement dans le handler HTTP.
D'abord, il faut désapprendre le réflexe du try/catch
Go n'a pas d'exceptions pour le flux de contrôle normal. Une fonction qui peut échouer retourne explicitement une valeur du type error, qui n'est en réalité rien de plus qu'une interface avec une seule méthode : Error() string. N'importe quel type qui implémente cette méthode peut servir d'erreur.
Cette simplicité a une conséquence directe : vérifier une erreur, c'est vérifier une valeur (if err != nil), pas intercepter une exception qui remonterait silencieusement la pile d'appels.
Une fois ce principe posé, comment distinguer différents types d'échecs ?
Deux approches complémentaires existent. La première, les erreurs sentinelles (var ErrNonTrouve = errors.New("ressource non trouvee")), sont des valeurs fixes qu'on compare directement. La seconde, les types d'erreur custom (une struct qui implémente Error()), transportent des données structurées en plus du message.
| Besoin | Outil | Fonction de vérification |
|---|---|---|
| Comparer à une erreur connue précise | Erreur sentinelle (errors.New) | errors.Is(err, ErrNonTrouve) |
| Extraire des données structurées | Type d'erreur custom | errors.As(err, &errValidation) |
| Ajouter du contexte sans perdre l'original | Wrapping | fmt.Errorf("...: %w", err) |
Prérequis
Il faut être à l'aise avec les interfaces (une leçon précédente) : error en est une, et comprendre les structs pour les types d'erreur custom.
Il reste une pratique essentielle en production : le wrapping
Quand une fonction appelle une autre fonction qui échoue, elle veut souvent ajouter du contexte ("chargement du profil 2000 a échoué") sans perdre l'information d'origine ("ressource non trouvée"). Le verbe %w dans fmt.Errorf fait exactement ça : il crée une nouvelle erreur qui "enveloppe" l'ancienne, consultable plus tard avec errors.Unwrap ou directement via errors.Is/errors.As, qui savent traverser toute la chaîne.
Piège fréquent
Utiliser %v au lieu de %w dans fmt.Errorf produit un message d'erreur qui a l'air identique à l'affichage, mais casse silencieusement la chaîne de wrapping : errors.Is et errors.As ne retrouveront alors plus jamais l'erreur d'origine. Cette différence d'un seul caractère est l'un des bugs les plus sournois du langage.
Enfin, ignorer une erreur reste le pire des pièges
Bonne pratique
Ne jamais écrire resultat, _ := faireQuelqueChose() sauf dans les rares cas où l'échec est réellement impossible ou sans conséquence. C'est le contrat implicite le plus important de Go : chaque error retournée doit être considérée, ne serait-ce que pour décider consciemment de l'ignorer.
Maintenant que tu sais gérer l'échec de façon structurée, la suite logique du cours change complètement de registre : place à la concurrence, le point fort historique de Go, en commençant par les goroutines.
Commandes & code
Gestion d'erreurs idiomatique
Go n'a pas d'exceptions pour le flux normal : l'erreur est une valeur, explicitement vérifiée.
package main
import (
"errors"
"fmt"
)
// error est une interface : type any avec une methode Error() string
type ErreurValidation struct {
Champ string
Message string
}
func (e *ErreurValidation) Error() string {
return fmt.Sprintf("champ %q : %s", e.Champ, e.Message)
}
// Erreurs sentinelles : valeurs comparables, exportees pour etre testees
var ErrNonTrouve = errors.New("ressource non trouvee")
var ErrNonAutorise = errors.New("acces non autorise")
func chercherUtilisateur(id int) (string, error) {
if id <= 0 {
return "", &ErreurValidation{Champ: "id", Message: "doit etre positif"}
}
if id > 1000 {
return "", ErrNonTrouve
}
return fmt.Sprintf("utilisateur-%d", id), nil
}
// Wrapping : enrichir une erreur tout en gardant la cause d'origine (%w)
func chargerProfil(id int) (string, error) {
u, err := chercherUtilisateur(id)
if err != nil {
return "", fmt.Errorf("chargement du profil %d: %w", id, err)
}
return "profil de " + u, nil
}
func main() {
// Verification systematique : "if err != nil" est LE pattern Go
profil, err := chargerProfil(2000)
if err != nil {
fmt.Println("erreur:", err)
// errors.Is : verifie si l'erreur enveloppe une erreur sentinelle precise
if errors.Is(err, ErrNonTrouve) {
fmt.Println("-> traiter comme un 404")
}
} else {
fmt.Println(profil)
}
_, err = chargerProfil(-5)
// errors.As : extrait un type d'erreur concret depuis la chaine de wrapping
var errValidation *ErreurValidation
if errors.As(err, &errValidation) {
fmt.Println("champ invalide:", errValidation.Champ)
}
// Deroulage manuel de la chaine d'erreurs
fmt.Println(errors.Unwrap(err) == nil) // ici la validation n'enveloppe rien
// Regrouper plusieurs erreurs (Go 1.20+)
err1 := errors.New("erreur 1")
err2 := errors.New("erreur 2")
combinee := errors.Join(err1, err2)
fmt.Println(combinee)
fmt.Println(errors.Is(combinee, err1), errors.Is(combinee, err2))
}Résumé
errorest une interface : n'importe quel type avecError() stringen fait office.- Erreurs sentinelles (
errors.New) comparées avecerrors.Is, types custom extraits avecerrors.As. fmt.Errorf("...: %w", err)enveloppe une erreur sans perdre la cause d'origine.- Ne jamais ignorer un
errorretourné : c'est le contrat implicite du langage.
Exercices pratiques
Mission : une API qui confond un ID invalide et une panne de base
Objectif : Faire remonter la bonne erreur HTTP selon la cause réelle d'un échec, en exploitant errors.Is/errors.As sans casser la chaîne de wrapping.
Contexte
Le handler HTTP GET /utilisateurs/{id} doit répondre 400 si l'ID est invalide, 404 si l'utilisateur n'existe pas, et 500 pour toute autre panne. Actuellement, il fait if err != nil { repondre500(err) } pour absolument tous les cas, ce qui renvoie une erreur serveur même pour un simple ID mal formé envoyé par un client.