Menu

Gestion des erreurs en Golang : if err != nil, wrapping, vérification

Go traite les erreurs comme des valeurs ordinaires renvoyées par les fonctions. Découvrez l'interface error, if err != nil, errors.New et fmt.Errorf, renvoyer des erreurs avec du contexte, les vérifier avec errors.Is et errors.As, et traiter chaque erreur une seule fois.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Les erreurs sont des valeurs

Une fonction Go qui peut échouer renvoie une error comme dernier résultat. L'appelant la vérifie immédiatement.

Sortie :

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

C'est tout le mécanisme. Il n'y a pas d'exceptions, pas de try ni de catch, et pas de flux de contrôle caché : une erreur ne va que là où votre code la transmet. Le prix, c'est la répétition visible de if err != nil. L'avantage, c'est que chaque point d'échec est visible dans le code, et que vous décidez à chacun ce qui se passe.

Le type error

error est une interface intégrée avec une seule méthode :

type error interface {
	Error() string
}

Tout type qui a une méthode Error() string est une erreur. Une erreur nil signifie succès. Afficher une erreur avec fmt.Println(err) ou %v appelle Error().

Créer des erreurs

Deux fonctions couvrent la plupart des cas.

errors.New crée une erreur au texte fixe. fmt.Errorf en formate une, avec les mêmes verbes que Printf. Par convention, les messages d'erreur commencent par une minuscule et ne finissent pas par une ponctuation, car ils sont généralement intégrés dans des messages plus longs : load config: open app.yaml: no such file or directory.

Le motif if err != nil

La forme idiomatique est : appeler, vérifier, retourner tôt. Le chemin de succès reste contre la marge gauche, et chaque échec sort dès qu'il se produit.

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

Deux conventions à remarquer :

  • En cas d'erreur, renvoyez la valeur zéro pour les autres résultats (nil, 0, ""). Les appelants ne doivent pas les utiliser quand err != nil.
  • if err := f(); err != nil limite la portée de err au if quand la fonction ne renvoie qu'une erreur. Cela garde la portée extérieure propre.

Évitez le else après un retour d'erreur. if err != nil { return err } else { ... } ne fait qu'indenter le chemin nominal sans raison.

Ajouter du contexte en renvoyant une erreur

Une erreur remontée telle quelle perd l'histoire de son origine. open config.yaml: no such file or directory ne vous dit pas quelle étape du démarrage a échoué. Ajoutez du contexte avec fmt.Errorf et le verbe %w :

Sortie :

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

Chaque couche ajoute ce qu'elle faisait, et le message final se lit comme une piste qui descend du haut de l'appel jusqu'à la cause. Un bon contexte nomme l'opération et l'entrée : parse line 12, fetch user 42. N'ajoutez pas « error » ou « failed » à chaque niveau ; le message est déjà une erreur.

%w enveloppe : il garde l'erreur d'origine dans la nouvelle, donc errors.Is et errors.As peuvent encore la retrouver. %v ne copie que le texte. Utilisez %v quand vous voulez délibérément cacher un détail d'implémentation aux appelants, par exemple pour qu'ils ne finissent pas par dépendre du type d'erreur d'un driver de base de données.

Vérifier des erreurs précises : errors.Is et errors.As

Parfois l'appelant doit réagir à un type d'échec précis : un fichier manquant signifie « utiliser les valeurs par défaut », un timeout signifie « réessayer ». Deux fonctions répondent à cela, et toutes deux traversent chaque couche d'enveloppement.

Règles pratiques :

  • Comparez aux valeurs d'erreur prédéfinies (des sentinelles comme io.EOF, os.ErrNotExist, sql.ErrNoRows) avec errors.Is, pas ==. == échoue dès que l'erreur est enveloppée.
  • Extrayez une erreur typée avec errors.As, pas une assertion de type, pour la même raison. errors.As prend un pointeur vers une variable du type cible.
  • Ne comparez jamais le texte de err.Error(). Les messages changent d'une version à l'autre, et la comparaison de texte casse alors silencieusement.

Définir vos propres erreurs sentinelles et types d'erreur, et joindre plusieurs erreurs avec errors.Join, est traité dans les erreurs personnalisées.

Traiter une erreur une seule fois

Une erreur doit être traitée exactement une fois. Traiter veut dire l'une de ces actions : la renvoyer (généralement enveloppée), la journaliser et continuer, réessayer, ou la transformer en réponse pour l'utilisateur. En faire deux est le bug d'erreur le plus courant dans le code Go.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

Journaliser puis renvoyer produit le même échec plusieurs fois dans les logs, chaque fois avec moins de contexte que le message final. Laissez les erreurs remonter jusqu'à l'endroit qui peut décider quoi faire (un handler HTTP, un main, une boucle de worker), et journalisez là.

Où finissent les erreurs

Au sommet du programme, quelque chose doit agir sur l'erreur. Dans main, cela veut généralement dire l'afficher et sortir avec un statut non nul :

Lancé sans argument, ce programme affiche error: usage: app <name> sur stderr et sort avec le statut 1 (tapez un nom dans le panneau Args pour voir l'autre chemin). Garder main sous cette forme, avec le vrai travail dans run, rend le programme testable et garantit que les instructions defer de run s'exécutent, puisque os.Exit saute les appels différés.

Dans un serveur HTTP, le sommet est le handler : il fait correspondre l'erreur à un code de statut et à un message sans risque pour le client, et journalise le message détaillé pour vous.

Les erreurs qu'on peut ignorer, et celles qu'on ne doit pas

Ignorer une erreur est parfois correct, mais rendez-le explicite avec _ pour que les lecteurs sachent que c'est une décision :

_ = conn.SetDeadline(t) // best effort

Certains appels ne peuvent pas échouer en pratique (strings.Builder.WriteString, bytes.Buffer.Write). D'autres semblent anodins et ne le sont pas : Close sur un fichier que vous avez écrit peut signaler que les données n'ont jamais atteint le disque, et json.Marshal échoue sur les channels et les fonctions. Dans le doute, vérifiez.

Le linter errcheck (inclus dans golangci-lint) signale les erreurs non vérifiées. go vet ne les signale pas à lui seul.

Erreurs et panics

Go a aussi panic, mais ce n'est pas un système d'exceptions. Utilisez les erreurs pour tout ce qui peut mal tourner en fonctionnement normal : entrée invalide, fichiers manquants, pannes réseau. Réservez panic aux bugs (un état impossible, un invariant cassé) et aux échecs au démarrage où continuer n'a aucun sens. Une bibliothèque ne devrait presque jamais provoquer de panic à travers son API. Voir panic et recover.

Réduire la répétition

if err != nil est verbeux, et les propositions d'ajouter une nouvelle syntaxe pour cela ont été refusées à plusieurs reprises ; l'équipe Go a annoncé en 2025 qu'elle ne poursuivait plus de changements de syntaxe pour la gestion des erreurs. Quelques motifs réduisent le bruit sans sortir du langage :

  • Retournez tôt et gardez des fonctions courtes. La plupart des répétitions viennent de longues fonctions qui enchaînent beaucoup d'étapes.
  • L'erreur collante. Pour une suite d'écritures, gardez la première erreur dans un champ de struct et faites des appels suivants des no-ops une fois qu'elle est définie. bufio.Writer fonctionne ainsi : vous vérifiez l'erreur une seule fois après Flush.
  • Enveloppez une fois par fonction. Une closure différée sur un résultat nommé peut ajouter le même contexte à chaque erreur renvoyée par la fonction (voir defer).

Erreurs courantes

  • Utiliser une valeur quand err n'est pas nil. Vérifiez d'abord, utilisez ensuite.
  • Journaliser et renvoyer. Choisissez l'un des deux.
  • Comparer avec == après un enveloppement. Utilisez errors.Is.
  • Perdre la cause avec %v. Utilisez %w, sauf si la masquer est justement le but.
  • Renvoyer un pointeur nil typé comme error. var e *MyErr; return e n'est pas nil pour l'appelant. Renvoyez un nil littéral.
  • Des messages avec majuscule ou ponctuation. errors.New("Failed to connect.") se lit mal une fois enveloppé. Écrivez connect to db: ....

Questions fréquentes

Comment fonctionne la gestion des erreurs en Go ?

Les fonctions qui peuvent échouer renvoient une error comme dernier résultat. L'appelant la vérifie tout de suite : v, err := f(); if err != nil { return err }. Une error est une valeur d'interface ordinaire avec une seule méthode, Error() string, et nil signifie succès. Il n'y a pas d'exceptions.

Go a-t-il try/catch ?

Non. Go n'a ni exceptions ni try/catch. Les échecs prévus sont renvoyés sous forme de valeurs error et vérifiés avec if err != nil. panic et recover existent, mais ils servent aux bugs de programmation et aux états irrécupérables, pas au flux normal des erreurs.

Comment renvoyer une erreur en Go ?

Déclarez error comme dernier résultat et renvoyez nil en cas de succès. Créez les erreurs avec errors.New("message") pour un texte fixe ou fmt.Errorf("reading %s: %w", name, err) pour ajouter du contexte à une erreur reçue. En cas d'échec, renvoyez des valeurs zéro pour les autres résultats.

Quelle est la différence entre %w et %v dans fmt.Errorf ?

Les deux insèrent le message de l'erreur d'origine dans la nouvelle. %w l'enveloppe aussi, donc errors.Is et errors.As peuvent encore retrouver l'original. %v produit une nouvelle erreur avec seulement le texte. Utilisez %w quand les appelants peuvent avoir besoin de vérifier la cause, et %v quand vous voulez la masquer.

Comment vérifier quelle erreur a été renvoyée en Go ?

Utilisez errors.Is(err, target) pour comparer à une erreur sentinelle comme io.EOF ou os.ErrNotExist, et errors.As(err, &target) pour extraire un type d'erreur précis comme *fs.PathError. Les deux parcourent les erreurs enveloppées. Évitez de comparer les chaînes err.Error().

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER