Contactez-nous

Comment gérer les exceptions avec `begin/rescue`

Ne laissez plus une erreur inattendue faire crasher votre application Ruby. Apprenez à utiliser la structure `begin/rescue/end` pour intercepter et gérer élégamment les exceptions, comme les erreurs de fichier ou de réseau.

Que se passe-t-il lorsqu'une erreur imprévue survient ?

Par défaut, lorsqu'une opération impossible est tentée dans votre code, comme diviser un nombre par zéro ou essayer de lire un fichier qui n'existe pas, Ruby lève ce qu'on appelle une exception. Une exception est un objet qui signale qu'une condition d'erreur s'est produite. Si cette exception n'est pas gérée, le comportement par défaut est simple et brutal : votre programme s'arrête net et affiche un message d'erreur (la fameuse 'stack trace').

Pensez à une exception comme à une alarme incendie dans un bâtiment. Quand elle se déclenche, tout s'arrête et une procédure d'urgence doit être suivie. Si aucune procédure n'est prévue (le code n'est pas géré), c'est la panique et l'évacuation (le crash du programme). La gestion des exceptions consiste à définir cette procédure d'urgence pour que le programme puisse réagir de manière contrôlée, sans s'arrêter complètement.

Comment intercepter une exception avec la syntaxe `begin/rescue` ?

Pour "attraper" une exception et l'empêcher de faire crasher votre programme, Ruby fournit la structure begin...rescue...end. C'est le mécanisme de base pour la gestion des erreurs.

Voici sa structure la plus simple :

  • Le bloc begin : vous placez ici le code qui est susceptible de lever une exception. Ruby va tenter de l'exécuter.
  • Le bloc rescue : si une exception est levée dans le bloc begin, Ruby arrête immédiatement l'exécution de ce bloc et saute directement au code contenu dans le bloc rescue.
  • Le mot-clé end : il marque la fin de la structure.

Prenons l'exemple d'une division par zéro qui, normalement, fait planter le script :

begin
  puts "Je vais tenter une opération risquée..."
  resultat = 10 / 0 # Cette ligne lève une ZeroDivisionError
  puts "Cette ligne ne sera jamais atteinte."
rescue
  puts "Oups ! Une erreur s'est produite, mais le programme continue."
end

puts "Le programme est arrivé à la fin sans crasher."
#=> Je vais tenter une opération risquée...
#=> Oups ! Une erreur s'est produite, mais le programme continue.
#=> Le programme est arrivé à la fin sans crasher.

Comme vous le voyez, le programme n'a pas planté. Le code dans le bloc rescue a été exécuté et le script a pu continuer jusqu'à la fin.

Pourquoi est-il crucial de spécifier le type d'erreur à intercepter ?

Utiliser rescue sans préciser le type d'erreur, comme dans l'exemple précédent, est généralement une mauvaise pratique. C'est ce qu'on appelle un "rescue à nu" (bare rescue). Pourquoi ? Parce qu'il intercepte toutes les exceptions, y compris les fautes de frappe dans votre code (NoMethodError) ou des erreurs système plus graves. Cela peut masquer des bugs que vous devriez en fait corriger.

Conseil de pro : soyez toujours aussi spécifique que possible dans vos clauses rescue. La bonne pratique consiste à spécifier la classe de l'exception que vous souhaitez intercepter. Vous pouvez également capturer l'objet exception dans une variable pour inspecter son message.

begin
  # ... code risqué ...
  resultat = 10 / 0
rescue ZeroDivisionError => e
  puts "Erreur interceptée ! Il est impossible de diviser par zéro."
  puts "Message d'erreur système : #{e.message}"
end

#=> Erreur interceptée ! Il est impossible de diviser par zéro.
#=> Message d'erreur système : divided by 0

Ici, nous n'attrapons que les erreurs de type ZeroDivisionError. Si une autre erreur, comme une TypeError, survenait, elle ne serait pas interceptée par ce bloc, ce qui est le comportement souhaité. La variable e contient l'instance de l'exception, nous permettant d'accéder à des informations utiles comme son message.

Mise en situation : gérer l'ouverture d'un fichier inexistant

Un cas d'usage très courant pour la gestion d'exceptions est la manipulation de fichiers. Que se passe-t-il si vous essayez de lire un fichier qui a été supprimé ou qui n'a jamais existé ? Ruby lève une exception Errno::ENOENT (Error Number: No Entity).

Dans une application réelle, vous ne voulez pas que votre programme s'arrête brutalement. Vous préférez afficher un message clair à l'utilisateur. Voici comment le gérer proprement :

filename = "rapport_inexistant.txt"

begin
  file_content = File.read(filename)
  puts "Contenu du fichier :"
  puts file_content
rescue Errno::ENOENT
  puts "Désolé, le fichier '#{filename}' n'a pas été trouvé."
  puts "Veuillez vérifier le nom et l'emplacement du fichier."
end

Cette approche transforme une erreur système abrupte en une expérience utilisateur contrôlée et informative. C'est la marque d'un logiciel robuste et professionnel.

Aller plus loin : quand utiliser `else` et `ensure` ?

La structure begin/rescue peut être complétée par deux clauses optionnelles : else et ensure.

  • Le bloc else : son code n'est exécuté que si aucune exception n'a été levée dans le bloc begin. C'est utile pour séparer le code qui peut échouer du code qui ne doit s'exécuter qu'en cas de succès.
  • Le bloc ensure : son code est exécuté toujours, qu'une exception ait été levée ou non, et que cette exception ait été interceptée ou non. Il est parfait pour les opérations de nettoyage, comme fermer un fichier ou une connexion réseau, pour garantir que les ressources sont toujours libérées.

Voici une structure complète pour illustrer le flux :

file = nil
begin
  file = File.open("mon_fichier.txt", "w")
  file.puts "Hello"
  # Décommentez la ligne ci-dessous pour simuler une erreur
  # raise "Une erreur s'est produite !"
rescue => e
  puts "Une erreur a été gérée : #{e.message}"
else
  puts "Le fichier a été écrit avec succès."
ensure
  if file
    file.close
    puts "Le fichier a été fermé."
  end
end

Maîtriser begin/rescue/else/ensure vous donne un contrôle total sur la gestion des erreurs et des ressources, une compétence indispensable pour écrire des applications Ruby fiables.