Excepciones

Generando una excepción

Una excepción es una clase de objeto especial, una instancia de la clase Exception o de una clase descendiente de esa clase que representa una condición especial; indica que algo ha salido mal. Cuando esto ocurre, se genera una excepción. Por defecto, los programas Ruby terminan cuando una excepción ocurre pero es posible escribir código que maneje estas excepciones.

Este código es un bloque que es ejecutado si una excepción ocurre durante la ejecución de otro bloque de código. Generar una excepción significa detener la ejecución normal del programa y transferir el control al código que maneja la excepción en donde puedes ocuparte del problema que ha sido encontrado o bien, terminar la ejecución del programa. Una de estas dos opciones (solucionar el problema de alguna manera o detener la ejecución de programa) ocurre dependiendo de si has praparcionado una cláusula rescue. Si no has proporcionado dicha cláusula, el programa termina, por el contrario, si la cláusula existe, el control de ejecución fluye hacia esta.

Ruby tiene algunas clases predefinidas -descendientes de la clase Exception- que te ayudan a manejar errores que ocurren en tu programa. La siguiente figura muestra la jerarquía de excepciones: 1

La tabla anterior muestra que la mayoría de las subclases extienden a la clase StandardError. Estas son excepciones "normales" que los programas Ruby tratan de manejar. Las otras excepciones representan errores más serios de bajo nivel que los programas Ruby normales no intentan manejar.

El siguiente método genera una excepción cada vez que es llamado. El segundo mensaje nunca va a ser mostrado. p43genera.rb

1 def genera_excepcion
2         puts 'Antes de la excepcion.'
3         raise 'Ha ocurrido un error'
4         puts 'Despues de la excepcion'
5 end
6 
7 genera_excepcion

El resultado es:

El método raise está definido en el módulo Kernel. Por defecto, raise genera una excepción de la clase RuntimeError. Para generar una excepción de una clase en específico, puedes pasar el nombre de la clase como argumento al método raise. p044inverso.rb

1 def inverse(x)
2   raise ArgumentError, 'El argumento no es un numero' unless x.is_a? Numeric
3   1.0 / x
4 end
5 puts inverse(2)
6 puts inverse('hola')

El resultado es:

Recuerda, los métodos que funcionan como preguntas (que regresan verdadero o falso), con frecuencia son nombrados con un ? al final. is_a? es un método de la clase Object que regresa true o false. Cuando unless es utilizado al final de una expresión, quiere decir: ejecuta la expresión anterior a menos que la condición sea verdadera.

Definiendo nuevas clases de excepciones: Para ser más específicos acerca de un error, puedes definir tu propia subclase de Exception:

1 class NoReversibleError < StandandError
2 
3 end

Manejando una excepción

Para manejar excepciones (handle exceptions), incluímos el código que pueda generar una excepción en un bloque begin-end y usamos una o más cláusulas rescue para indicarle a Ruby los tipos de excepción que queremos manejar. Es importante notar que el cuerpo de la definición de un método es un bloque begin-end explícito; begin es omitido y todo el cuerpo de la definición del método está sujeto al manejo de excepciones hasta que aparezca la palabra end.

El programa p045manejandoexcep.rb ilustra lo anterior:

 1 def genera_y_rescata
 2   begin        
 3           puts 'Estoy antes de raise.'
 4                 raise 'Ha ocurrido un error.'
 5                 puts 'Estoy despues de raise.'
 6         rescue
 7           puts 'He sido rescatado.'
 8         end
 9         puts 'Estoy despues de begin.'
10 end
11 genera_y_rescata

El resultado es:

Observa que el código interrumpido por la excepción nunca es ejecutado. Una vez que la excepción es rescatada, la ejecución continúa inmediatamente después del bloque begin que la generó.

Si escribes una cláusula rescue sin lista de parámetros, el parámetro por defecto es StandardError. Cada cláusula rescue puede especificar múltiples excepciones que rescatar. Al final de cada cláusula rescue puedes darle a Ruby el nombre de una varible local para que reciba la excepción. Los parámetros de la cláusula rescue pueden ser también expresiones arbitrarias (incluyendo llamadas a métodos) que regresan una clase Exception. Si utilizamos raise sin parámetros, genera la excepción.

Puedes apilar cláusulas rescue en un bloque begin-end. Las excepciones que no sean manejadas por una cláusula rescue fluirán hacia la siguiente:

 1 begin
 2   # ...
 3 rescue UnTipoDeExcepcion
 4   # ...
 5 rescue OtroTipoDeExcepcion
 6   # ..
 7 else
 8   # Otras excepciones
 9 end

Para cada cláusula rescue en el bloque begin, Ruby compara la excepción generada con cada uno de los parámetros en turno. La ocurrencia tiene éxito si la excepción nombrada en la cláusula rescue es del mismo tipo que la excepción generada. El código en una cláusula else es ejecutado si el código en la expresiôn begin es ejecutado sin excepciones. Si una excepción ocurre, entonces la cláusula else no es ejecutada. El uso de una cláusula eles no es particularmente común en Ruby.

Si quieres interrogar a una excepción rescatada, puedes asignar el objeto de clase Exception a una variable en la cláusula rescue, como se muestra en el programa p046excpvar.rb

1 begin
2   raise 'Una excepcion.'
3 rescue Exception => e
4   puts e.message
5         puts e.backtrace.inspect
6 end

El resultado es:

La clase Exception define dos métodos que regresan detalles acerca de la excepción. El método message regresa una cadena que puede proporcionar detalles legibles acerca de lo que ocurrió mal. El otro método importante es backtrace. Este método regresa un array de cadenas que representa la pila de ejecución hasta el punto en que la excepción fue generada.

Si necesitas garantizar que algún proceso es ejecutado al final de un bloque de código sin importar si se generó una excepción o no, puedes usar la cláusula ensure. ensure va al final de la última cláusula rescue y contiene un bloque de código que siempre va a ser ejecutado.

Algunas excepciones comunes se muestran en la siguiente tabla, cortesía del libro Ruby for Rails:

Ejemplo: Vamos a modificar el programa p027.leeryescribir.rb para incluír manejo de excepciones como se muestra en el ejemplo p046leeryescribir.rb

 1 # Abrir un archivo y leer su contenido
 2 # Nota que ya que esta presente un bloque, el archivo
 3 # es cerrado automaticamente cuando se termina la ejecucion
 4 # del bloque
 5 begin
 6   File.open('p014estructuras.rb', 'r') do |f1|
 7     while linea = f1.gets
 8       puts linea
 9     end
10   end
11 
12   # Crer un archivo y escribir en el
13   File.open('prueba.txt', 'w') do |f2|
14     f2.puts "Creado desde un programa Ruby!"
15   end
16 rescue Exception => msg
17   # mostar el mensaje de error generado por el sistema
18   puts msg
19 end

Mensajes de error inapropiados pueden proporcionar información crítica de una aplicación que puede ayudar a personas que quieran atacar nuestra aplicación. El problema más común ocurre cuando un mensaje de error detallados como pilas de ejecución, información de la base de datos y códigos de error son mostrados al usuario. Los analistas en seguridad ven al manejo de excepciones y al logging como áreas potenciales de riesgo. Es recomendable que las aplicaciones en producción no utilicen, por ejemplo, llamadas a puts e.backtrace.inspect a menos que sean dirigidas directamente a un log que no es visible para los usuarios finales.

Ejemplo de validación

Este es un ejemplo del libro Ruby Cookbook que muestro cómo podemos realizar validaciones de datos proporcionados por el usuario.

 1 class Nombre
 2   
 3   # Definir metodos getter pero no metodos setter
 4   attr_reader :nombre, :apellido
 5   
 6   # Establocer reglas para cuando alguien trata de 
 7   # asignar un nombre
 8   
 9   def nombre=(nombre)
10     if nombre == nil or nombre.size == 0
11       raise ArgumentError.new("Todos las personas deben tener un nombre")
12     end
13     nombre = nombre.dup
14     nombre[0] = nombre[0].chr.capitalize
15     @nombre = nombre
16   end
17   
18   # Establocer reglas para cuando alguien trata de 
19   # asignar un apellido
20   
21   def apellido=(apellido)
22     if apellido == nil or apellido.size == 0
23       raise ArgumentError.new("Todas las personas deben tener un apellido")
24     end
25     @apellido = apellido
26   end
27   
28   def nombre_completo
29     "#{@nombre} #{@apellido}"
30   end
31   
32   # Delegar a los metodos setter en vez de asignar
33   # las variables de instancia directamente
34   
35   def initialize(nombre, apellido)
36     self.nombre = nombre
37     self.apellido = apellido
38   end
39 end
40   
41 jacobo = Nombre.new('Jacobo', 'Berendes')
42 jacobo.nombre = 'Mary Sue'
43 puts jacobo.nombre_completo
44 
45 john = Nombre.new('john', 'von Neumann')
46 puts john.nombre_completo
47 john.nombre = nil #genera una excepcion
48 
49 Nombre.new('Carmen', nil) #genera una excepcion

La clase Nombre registra los nombres y apellidos de personas. Utiliza metodos setter para forzar dos reglas: todas las personas deben tener nombres y apellidos y todos los nombres deben comenzar con una letra mayúscula. La clase Nombre ha sido escrita de tal manera que estas reglas se aplican tanto en el método constructor como una vez que el objeto ha sido creado. Algunas veces no puedes confiar en los datos que vienen de los métodos setter. Es entonces cuando puedes definir tus propios métodos para dener datos corruptos antes de que infecte tus objetos.

Dentro de una clase, tienes acceso directo a las variables de instancia. Puedes simplemente asignar a variables de instancia y los métodos setter no son ejecutados. Si quieres que se ejecute el método setter debes llamarlo explícitamente. Nota come en el método Nombre#initialize anterior, llamamos a los métodos nombre= y apellido= en lugar de asignarlos directamente a @nombre y @apellido. De esta manera nos aseguramos que los métodos que contienen la validación son ejecutados.

1 Fuente: Programming Ruby