Retour au cours

games / lua

Gestion d'erreurs : pcall et xpcall

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Lever une erreur volontairement avec error(), avec une chaîne ou une table structurée
  • Capturer une erreur sans faire planter le programme grâce à pcall
  • Utiliser xpcall pour capturer une stack trace complète au bon moment
  • Vérifier une précondition en une ligne avec assert
  • Construire un wrapper générique qui logue systématiquement les erreurs capturées

Dans quel contexte ?

Un script serveur qui traite des paiements en jeu ne doit jamais s'arrêter complètement à cause d'une seule requête malformée envoyée par un joueur : une division par un montant nul, ou une donnée manquante, ne doit affecter que cette transaction précise, jamais faire planter tout le serveur pour tous les autres joueurs connectés. C'est exactement ce que pcall rend possible.

FonctionQuand l'utiliser
error(msg)Signaler une situation anormale, interrompre la fonction courante
pcall(fn, ...)Appeler une fonction sans risquer de faire planter l'appelant
xpcall(fn, handler)Capturer une stack trace complète au moment de l'erreur
assert(cond, msg)Vérifier rapidement une précondition

Piège fréquent

Oublier de vérifier le premier retour de pcall (ok) avant d'utiliser le second est une erreur fréquente : si ok est false, la seconde valeur est un message d'erreur, pas un résultat exploitable normalement.

Une philosophie différente de la gestion d'erreurs

De nombreux langages proposent un bloc try/catch pour capturer une erreur. Lua ne l'a pas — à la place, il propose deux fonctions ordinaires (pcall et xpcall) qui font le même travail, mais en s'intégrant naturellement dans la logique du langage : appeler une fonction n'importe où, protégée ou non, se fait toujours de la même façon.

error() : interrompre l'exécution normale

Quand une fonction rencontre une situation qu'elle ne peut pas gérer elle-même (une division par zéro, une valeur invalide), elle appelle error(), qui interrompt immédiatement son exécution et "remonte" l'information vers celui qui l'a appelée. Sans protection, cette remontée continue jusqu'à faire planter tout le programme — exactement comme une exception non capturée dans d'autres langages.

pcall : appeler une fonction sans risquer de tout faire planter

pcall (protected call, "appel protégé") exécute une fonction en interceptant toute erreur qu'elle pourrait lever, plutôt que de laisser cette erreur se propager. Il renvoie toujours deux informations : un booléen indiquant si tout s'est bien passé, puis soit le résultat normal (si succès), soit le message d'erreur (si échec). Ce pattern à deux valeurs de retour est la façon idiomatique de gérer les erreurs en Lua : on vérifie toujours le premier retour avant d'utiliser le second.

xpcall : capturer le contexte au bon moment

xpcall ajoute une subtilité importante : il accepte une fonction "handler" appelée exactement au moment où l'erreur survient — avant que la pile d'appels ne soit "dépilée" (nettoyée). C'est crucial pour capturer une trace complète des appels de fonctions (debug.traceback), car cette information disparaît dès que la pile est nettoyée ; pcall seul arrive trop tard pour la récupérer.

assert : une erreur volontaire, en une ligne

assert(condition, message) est un raccourci qui lève une erreur seulement si la condition est fausse — pratique pour vérifier rapidement une précondition (un fichier bien ouvert, un paramètre valide) sans écrire un if explicite à chaque fois.

Commandes & code

Gestion d'erreurs : pcall et xpcall

Lua n'a pas de try/catch : la gestion d'erreurs repose sur error() pour lever, et pcall/xpcall pour capturer.

lua
-- error() lève une erreur, interrompt l'exécution normale de la fonction courante
local function divide(a, b)
  if b == 0 then
    error("division par zéro")   -- interrompt immédiatement divide(), remonte l'appelant
  end
  return a / b
end

-- Sans protection : un appel direct planterait tout le programme
-- print(divide(10, 0))   -- crash: "chemin:ligne: division par zéro"
lua
-- pcall (protected call) : exécute une fonction en capturant toute erreur, ne plante jamais l'appelant
local ok, result = pcall(divide, 10, 0)
print(ok, result)   -- false   "chemin:ligne: division par zéro"

local ok2, result2 = pcall(divide, 10, 2)
print(ok2, result2)   -- true   5.0

-- Pattern idiomatique de gestion
local ok, resultOrError = pcall(divide, 10, 0)
if ok then
  print("Résultat: " .. resultOrError)
else
  print("Erreur capturée: " .. resultOrError)
end
lua
-- error() avec un niveau de traçage (2ème argument) pour pointer l'erreur vers l'APPELANT, pas la fonction elle-même
local function validateAge(age)
  if age < 0 then
    error("l'âge ne peut pas être négatif", 2)   -- niveau 2 : le message pointera vers la ligne d'appel de validateAge
  end
end
lua
-- error() peut aussi lever une TABLE (pas juste une string), utile pour des erreurs structurées
local function fetchResource(id)
  if id < 1 then
    error({ code = "INVALID_ID", message = "id doit être positif", id = id })
  end
end

local ok, err = pcall(fetchResource, -5)
if not ok and type(err) == "table" then
  print("Code d'erreur: " .. err.code .. " (id=" .. err.id .. ")")
end
lua
-- xpcall : comme pcall, mais avec un "message handler" appelé AU MOMENT de l'erreur
-- (utile pour capturer une stack trace, avant que la pile ne soit dépilée)
local function riskyOperation()
  error("problème pendant l'opération")
end

local function errorHandler(err)
  return debug.traceback(err, 2)   -- enrichit le message avec la pile d'appels complète
end

local ok, traceback = xpcall(riskyOperation, errorHandler)
if not ok then
  print(traceback)   -- affiche le message ET la trace complète des appels de fonctions
end
lua
-- Pattern "assert" : raccourci pour vérifier une condition et lever une erreur si elle échoue
local function loadConfig(path)
  local file = io.open(path, "r")
  assert(file, "impossible d'ouvrir le fichier de configuration: " .. path)   -- lève une erreur si file est nil
  local content = file:read("*a")
  file:close()
  return content
end

-- assert() retourne ses arguments tels quels si la condition est truthy, pratique pour chaîner
local config = assert(loadConfig("config.lua"))
lua
-- Pattern robuste : wrapper générique pour exécuter une opération avec logging systématique des erreurs
local function safeCall(fn, ...)
  local results = { pcall(fn, ...) }
  local ok = table.remove(results, 1)
  if not ok then
    local err = results[1]
    print("[ERREUR] " .. tostring(err))
    -- ici : envoyer à un système de monitoring/logging externe en production
    return nil
  end
  return table.unpack(results)   -- ré-étale les valeurs de retour multiples de fn
end

local value = safeCall(divide, 10, 0)   -- ne plante pas, log l'erreur, retourne nil

Résumé

  • error() lève une erreur (string ou table), interrompant l'exécution normale jusqu'au pcall englobant.
  • pcall capture toute erreur sans faire planter le programme appelant ; le premier retour indique le succès.
  • xpcall ajoute un handler d'erreur exécuté AVANT le dépilement de la pile, idéal pour capturer une stack trace.
  • assert(condition, message) est un raccourci pratique pour transformer une condition invalide en erreur explicite.

Exercices pratiques

1 disponible
1

Mission : le paiement qui fait planter tout le serveur

Objectif : Protéger un système de paiement en jeu contre une transaction malformée sans faire planter le reste du serveur, et diagnostiquer une mauvaise utilisation de pcall.

Contexte

Un script de paiement en jeu contient cette fonction, appelée à chaque transaction :

lua
local function processPayment(amount, taxRate)
  if taxRate == 0 then
    error("taux de taxe invalide")
  end
  return amount / taxRate
end

-- Appelé directement, sans protection, pour chaque transaction reçue
local total = processPayment(100, 0)   -- fait planter TOUT le serveur pour TOUS les joueurs
print("Total: " .. total)

Un joueur a envoyé une requête avec un taxRate à 0 (bug côté client), et le serveur entier a crashé, déconnectant tous les joueurs en cours de partie. Un développeur a ensuite ajouté un pcall, mais mal utilisé :

lua
local result = pcall(processPayment, 100, 0)
print("Total: " .. result)   -- affiche "Total: false" au lieu de gérer l'erreur !
Résoudre l’exercice →