La gestion des erreurs
Ce chapitre présente la gestion des exceptions, le mécanisme qui permet de réagir quand quelque chose tourne mal dans un programme. Au lieu de crash silencieux ou de valeurs nil inexpliquées, on apprend à lever, capturer et traiter les erreurs proprement.
Principe
Imaginons qu'on essaie de charger un fichier de sauvegarde qui n'existe pas, ou qu'un utilisateur entre un niveau négatif pour un Pokémon. Sans gestion d'erreurs, le programme crash avec un message cryptique.
En Ruby, les erreurs sont des objets. Quand quelque chose tourne mal, on lève (raise) une exception. Le programme remonte la pile d'appels jusqu'à trouver un bloc rescue capable de la capturer. Si aucun rescue ne la capture, le programme s'arrête.
Ce mécanisme sépare le code normal du code de gestion d'erreurs : on écrit le chemin normal, et on traite les cas problématiques dans des blocs dédiés.
Lever une exception avec raise
raise interrompt l'exécution et crée un objet exception :
# Forme simple : crée un RuntimeError
raise 'Pokémon introuvable'
# Avec une classe spécifique
raise ArgumentError, 'Le niveau doit être entre 1 et 100'
raise 'message'crée unRuntimeErrorpar défaut.raise ArgumentError, 'message'crée une erreur d'un type précis. C'est la forme recommandée car elle permet de capturer les erreurs par type.- Une fois levée, l'exception interrompt tout : les lignes suivantes ne sont pas exécutées.
Capturer avec begin...rescue...end
Le bloc begin...rescue...end capture les exceptions :
Affiche :
Erreur : Le niveau doit être positif
Le programme continue normalement.
rescue ArgumentError => errorcapture lesArgumentErroret stocke l'objet danserror.error.messageretourne le message passé àraise.- Le code après le
ends'exécute normalement : le programme ne crash pas.
rescue dans une méthode
Quand le rescue couvre toute la méthode, on peut se passer de begin :
def create_pokemon(name, level)
raise ArgumentError, 'Le nom ne peut pas être vide' if name.empty?
raise ArgumentError, 'Le niveau doit être entre 1 et 100' unless (1..100).include?(level)
return { name: name, level: level }
rescue ArgumentError => error
puts "Erreur de création : #{error.message}"
return nil
end
result = create_pokemon('', 25)
puts result.inspect # => nil
result = create_pokemon('Pikachu', 25)
puts result.inspect # => {:name=>"Pikachu", :level=>25}
- Le
rescues'applique à tout le corps de la méthode. - On retourne
nildans lerescuepour que l'appelant puisse vérifier si la création a réussi.
Capturer plusieurs types d'exceptions
On peut enchaîner plusieurs rescue pour gérer différents types d'erreurs :
def load_pokemon(name)
raise ArgumentError, 'Nom vide' if name.empty?
raise RuntimeError, "Pokémon #{name} introuvable" unless name == 'Pikachu'
return { name: name, level: 25 }
rescue ArgumentError => error
puts "Argument invalide : #{error.message}"
return nil
rescue RuntimeError => error
puts "Erreur : #{error.message}"
return nil
end
- Ruby essaie chaque
rescuedans l'ordre. Le premier qui correspond est exécuté. - Toujours placer les exceptions les plus spécifiques en premier. Sinon, une clause générale les attraperait toutes.
La hiérarchie des exceptions
Les exceptions Ruby forment une hiérarchie de classes (comme l'héritage vu au chapitre 9) :
Exception
SignalException (Ctrl+C)
NoMemoryError
ScriptError
SyntaxError
LoadError
StandardError ← rescue capture ça par défaut
ArgumentError
RuntimeError
TypeError
NameError
NoMethodError
IOError
ZeroDivisionError
KeyError
IndexError
rescuesans type captureStandardErroret tous ses descendants. C'est le comportement correct dans 99% des cas.- Ne jamais écrire
rescue Exception: cela capture aussiSignalException(Ctrl+C), ce qui rend le programme impossible à interrompre.
retry — réessayer
retry reprend l'exécution au début du bloc begin. Il faut toujours un compteur pour éviter les boucles infinies :
attempts = 0
begin
attempts += 1
puts "Tentative #{attempts}..."
# Simuler une erreur aléatoire
raise 'Échec du chargement' if rand(3) != 0
puts 'Chargement réussi !'
rescue RuntimeError => error
puts "Erreur : #{error.message}"
retry if attempts < 3
puts 'Abandon après 3 tentatives.'
end
retryrevient aubeginet réexécute tout le bloc.- Sans le compteur
attempts < 3, la boucle serait infinie en cas d'erreur permanente. raisesans argument (seul) dans unrescuerelève l'exception courante, utile pour abandonner après les tentatives.
ensure — exécuter quoi qu'il arrive
Le bloc ensure s'exécute toujours, qu'une exception ait été levée ou non :
def save(data, filename)
puts 'Sauvegarde en cours...'
raise 'Erreur de sauvegarde' if data.nil?
puts "Sauvegardé dans #{filename}"
rescue RuntimeError => error
puts "Échec : #{error.message}"
ensure
puts 'Nettoyage terminé.'
end
save({ name: 'Pikachu' }, 'save.dat')
save(nil, 'save.dat')
Affiche :
Sauvegarde en cours...
Sauvegardé dans save.dat
Nettoyage terminé.
Sauvegarde en cours...
Échec : Erreur de sauvegarde
Nettoyage terminé.
ensures'exécute dans tous les cas : après le code normal, après unrescue, même après unreturn.- C'est l'endroit idéal pour fermer des fichiers, libérer des ressources, ou afficher un message final.
Créer ses propres exceptions
Pour les erreurs spécifiques à son programme, on crée des classes qui héritent de StandardError :
module Pokedex
class InvalidPokemonError < StandardError; end
class DuplicateError < StandardError; end
class TeamFullError < StandardError; end
end
On les utilise ensuite comme n'importe quelle exception :
def add_pokemon(team, pokemon)
raise Pokedex::TeamFullError, "L'équipe est pleine (6 max)" if team.size >= 6
raise Pokedex::DuplicateError, "#{pokemon} est déjà dans l'équipe" if team.include?(pokemon)
team << pokemon
return team
end
begin
team = ['Pikachu', 'Dracaufeu', 'Tortank', 'Florizarre', 'Dracolosse', 'Ectoplasma']
add_pokemon(team, 'Mewtwo')
rescue Pokedex::TeamFullError => error
puts error.message
end
Affiche : L'équipe est pleine (6 max)
- Hériter de
StandardErrorgarantit querescuesans type les capture. - Le nom de la classe décrit le problème :
TeamFullErrorest plus parlant queRuntimeError. - Regrouper les exceptions dans un module évite les conflits de noms.
Relever une exception
raise sans argument dans un rescue relève l'exception courante. C'est utile pour afficher un message tout en laissant l'erreur remonter :
def process_pokemon(data)
puts "Traitement de #{data[:name]}..."
raise 'Données corrompues' if data[:level].nil?
return data
rescue RuntimeError => error
puts "Échec : #{error.message}"
raise
end
raiseseul relève exactement la même exception. L'appelant devra la capturer à son tour.
Conclusion
raiselève une exception. Préférerraise ClassName, 'message'pour des erreurs typées.begin...rescue...endcapture les exceptions.rescuedans undeffonctionne sansbegin.- Placer les clauses
rescueles plus spécifiques en premier. - Ne jamais écrire
rescue Exception. CapturerStandardErrorou plus spécifique. retryreprend aubegin. Toujours protéger avec un compteur.ensures'exécute toujours, même après une erreur ou unreturn.- Créer des classes d'erreur qui héritent de
StandardErrorpour les erreurs métier. raisesans argument dans unrescuerelève l'exception courante.