Retour au cours

backend / go

Gestion d'erreurs idiomatique

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi error est une simple interface, pas un mécanisme spécial du langage
  • Créer des erreurs sentinelles avec errors.New et les comparer avec errors.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.

BesoinOutilFonction de vérification
Comparer à une erreur connue préciseErreur sentinelle (errors.New)errors.Is(err, ErrNonTrouve)
Extraire des données structuréesType d'erreur customerrors.As(err, &errValidation)
Ajouter du contexte sans perdre l'originalWrappingfmt.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.

go
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é

  • error est une interface : n'importe quel type avec Error() string en fait office.
  • Erreurs sentinelles (errors.New) comparées avec errors.Is, types custom extraits avec errors.As.
  • fmt.Errorf("...: %w", err) enveloppe une erreur sans perdre la cause d'origine.
  • Ne jamais ignorer un error retourné : c'est le contrat implicite du langage.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →