Les exceptions ? Qu'est-ce que c'est ?

Une exception est un type spécial d'objet, une instance de la classe Exception ou un descendant de cette classe. Lancer une exception (raise) sert à interrompre le chemin normal d'exécution du programme pour traiter le problème ou quitter le programme.

Le choix par l'interpréteur Ruby de quitter ou non le programme dépendra du fait que vous avez écrit ou non un bloc rescue pour le morceau de code qui a levé l'exception. Si vous n'avez pas fourni ce bloc, le programme quittera sinon l'interpréteur entrera dans votre bloc rescue pour prendre les mesures nécessaires.

Un exemple simple pourrait être : 1/0 qui lévera une exception du type ZeroDivisionError puisque vous avez tenté de diviser un nombre par zéro ce qui mathématiquement impossible. ZeroDivisionError est le nom d'une classe particulière d'exception qui hérite de la classe Exception.

Voici une liste des exceptions qu'on rencontre le plus souvent. Elles descendent toutes de la classe Exception.

Nom de l'exception Raison Exemple
RuntimeError Levé par défaut par raise raise
NoMethodError Un message inconnu a été envoyé à un objet Object.new.methode_inconnue
NameError Un mot n'a pu être résolu en tant que variable ou nom de méthode variable_inconnue
IOError Causé par la lecture d'un flux fermé, l'écriture sur un flux en lecture seule, etc STDIN.puts("Impossible d'écrire sur STDIN")
TypeError Une méthode a reçue un argument qu'elle ne peut pas gérer 3 + "un chaîne ..."
ArgumentError Levé par l'utilisation d'un nombre incorrect d'arguments def m(x); end; m(1, 2, 3)

rescue est là pour vous aider !

Comme je vous l'ai expliqué avant, lever une exception ne revient pas forcément à quitter le programme. Vous pouvez traiter les exceptions en solutionnant les problèmes ce qui vous permet de continuer l'exécution du programme. Pour cela vous devez utiliser le mot-clef rescue.

Deux méthodes vous permettent de gérer vos exceptions :

  • Entourer le code à protéger des mots-clefs begin/end
  • Protéger une méthode compléte en ajoutant un bloc rescue à la fin de la définition de cette méthode
Voici un petit exemple qui vous protégera des divisions par zéro :

print "Veuillez entrer un chiffre : "
n = gets.to_i

begin
  resultat = 100 / n
rescue
  puts "Avez-vous tenté une division par zéro ?"
  exit
end

puts "100/#{n} vaut #{resultat}."
		

Si vous tentez d'exécuter ce morceau de code en entrant, le programme va lever une exception de type ZeroDivisionError, notre code étant dans un bloc begin/rescue, la bloc rescue sera appelé et notre message d'erreur affiché. Si vous entrez une valeur correcte le code sera exécuté normalement et le bloc rescue ne sera pas interprété.

Vous pouvez également utiliser une clause rescue qui ne gérera qu'un type précis d'exception. Dans notre cas, nous aurions pu utiliser rescue ZeroDivisionError. Dans ce cas les autres exceptions ne seront pas attrapées par notre bloc rescue.

Et si je veux lever une exception dans mon code ?

Utiliser le système d'exception vous permettra d'avoir un code plus flexible et compact. Il est donc tout naturel que vous puissiez lever des exceptions dans votre propre code et même créer de nouvelles exceptions.

Pour lever une exception, il vous suffit d'utiliser le mot-clef raise. Vous pouvez éventuellement lui passer le nom de l'exception que vous voulez lever et une chaîne qui sera utilisée comme message d'erreur.

Voici un exemple d'utilisation :

def methode_explosive(x)
  raise ArgumentError, "Le chiffre est supérieur à 10 !" unless x < 10
end

methode_explosive(20)
	

On obtient donc : ArgumentError: Le chiffre est supérieur à 10 !

Vous pouvez bien évidemment gérer cette erreur :

begin
  methode_explosive(20)
rescue ArgumentError
  puts "Ce chiffre est trop grand !"
end
	

On obtient maintenant : Ce chiffre est trop grand !

Ici on ne traite que ArgumentError pour être sûr de ne faire notre traitement d'erreur que dans le cas précis d'une erreur d'argument. Les autres types d'erreurs ne seront pas traités et ne seront donc pas masqués par notre code de traitement des erreurs. Vous aurez compris qu'il faut éviter les rescue généralistes sous peine de vous retrouver avec des bugs introuvables puisque masqués.

Notez qu'il vous est possible de relancer une exception que vous avez traité. De cette façon, vous pourrez transmettre l'exception aux autres blocs rescue pour plus de traitement.

Dans notre exemple, on pourrait faire :

begin
  methode_explosive(20)
rescue ArgumentError
  puts "Ce chiffre est trop grand !"
  raise
end
	

On obtient maintenant :
Ce chiffre est trop grand !
ArgumentError: Le chiffre est supérieur à 10 !

D'accord, mais comment je crée mes propres exceptions ?

Il peut vous arriver d'avoir à créer vos propres exceptions pour plus de clarté (c'est ce qui est fait très massivement dans Rails par exemple). Il vous faudra donc créer une nouvelle classe d'exception héritant d'Exception ou de l'une de ses sous-classes.

En pratique, voilà comment on s'y prend :

class MonException < Exception
end

raise MonException, "Eh ! C'est ma nouvelle exception !"
	

L'un des avantages de créer vos propres classes d'exceptions, en plus de la clarté, est de pouvoir appeler vos exceptions par leur nom :

begin
  puts "On va lever notre nouvelle exception"
  raise MonException
rescue MonException => e
  puts "Une exception a été levée : #{e}"
end
	

inattendues

Ce qui nous donnera :
On va lever notre nouvelle exception
Une exception a été levée : MonException

Conclusion

Vous aurez donc compris qu'utiliser les exceptions vous permettra d'avoir un code plus flexible, qui sera capable de gérer des erreurs parfois difficilement récupérables autrement. Créer vos propres exceptions vous permettra également de mieux documenter votre code et d'afficher des messages plus précis aux utilisateurs. Alors n'hésitez plus et mettez vous aux exceptions ;-)