games / lua
Gestion d'erreurs : pcall et xpcall
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
xpcallpour 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.
| Fonction | Quand 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.
-- 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"-- 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-- 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-- 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-- 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-- 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"))-- 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 nilRésumé
error()lève une erreur (string ou table), interrompant l'exécution normale jusqu'aupcallenglobant.pcallcapture toute erreur sans faire planter le programme appelant ; le premier retour indique le succès.xpcallajoute 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
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 :
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é :
local result = pcall(processPayment, 100, 0)
print("Total: " .. result) -- affiche "Total: false" au lieu de gérer l'erreur !