Excepciones en Ruby: begin, rescue, ensure y raise
Una excepción es la forma que tiene Ruby de decir «aquí ha pasado algo que no puedo resolver»: dividir entre cero, abrir un fichero que no existe, llamar a un método que no está. Si nadie la recoge, el programa se para.
Es como la alarma de humo de una casa: salta en un punto concreto y alguien tiene que decidir qué hacer. Puedes apagar el fuego (capturar el error), ventilar pase lo que pase (ensure) o llamar a los bomberos (dejar que el error suba).
Capturar un error: begin y rescue
En Ruby, un error se captura con begin … rescue … end: el código que puede fallar va entre begin y rescue, y lo que hay que hacer si falla, después de rescue.
def dividir(a, b)
a / b
rescue ZeroDivisionError => error
puts "No se puede dividir entre cero (#{error.message})"
nil
end
dividir(10, 2) # => 5
dividir(1, 0) # No se puede dividir entre cero (divided by 0)
Dentro de un método no hace falta escribir begin: el propio def hace de bloque. Con => error guardas la excepción en una variable para consultar su clase (error.class), su mensaje (error.message) y su traza (error.backtrace, la lista de líneas por las que pasó).
¿Hay try y catch en Ruby?
Si vienes de Java, JavaScript o Python, buscarás try … catch. En Ruby se escribe con otras palabras, pero hace lo mismo:
| En otros lenguajes | En Ruby |
|---|---|
try |
begin (o el cuerpo de un def) |
catch / except |
rescue |
finally |
ensure |
throw / raise |
raise |
Ojo con la trampa: Ruby también tiene catch y throw, pero no son para errores. Lo verás al final del artículo.
Varias cláusulas rescue
Puedes tener varias, y cada una puede capturar varias clases de error. Ruby usa la primera que encaje, así que pon las más concretas arriba:
def evaluar(codigo)
eval(codigo)
rescue SyntaxError, NameError => error
puts "El código no compila: #{error.class}"
rescue StandardError => error
puts "Error al ejecutar: #{error.class}"
end
evaluar("1 +") # El código no compila: SyntaxError
evaluar("variable_que_no_existe") # El código no compila: NameError
evaluar("Integer('abc')") # Error al ejecutar: ArgumentError
La jerarquía de excepciones
Todas las excepciones son clases que heredan de Exception. Estas son las principales en Ruby 4.0:
Exception
├── NoMemoryError
├── ScriptError
│ ├── LoadError
│ ├── NotImplementedError
│ └── SyntaxError
├── SignalException
│ └── Interrupt
├── StandardError
│ ├── ArgumentError
│ ├── EncodingError
│ ├── FiberError
│ ├── IOError
│ │ └── EOFError
│ ├── IndexError
│ │ ├── KeyError
│ │ └── StopIteration
│ ├── LocalJumpError
│ ├── NameError
│ │ └── NoMethodError
│ ├── RangeError
│ │ └── FloatDomainError
│ ├── RegexpError
│ ├── RuntimeError
│ │ └── FrozenError
│ ├── SystemCallError
│ │ └── Errno::ENOENT, Errno::EACCES…
│ ├── ThreadError
│ ├── TypeError
│ └── ZeroDivisionError
├── SystemExit
└── SystemStackError
Lo importante: un rescue sin clase solo captura StandardError y sus hijas. Es a propósito: errores como Interrupt (Ctrl + C) o SystemExit (exit) deben poder parar el programa. Por eso, si creas tus propias excepciones, haz que hereden de StandardError; si no, un rescue normal no las capturará.
Ejecutar algo siempre: ensure
ensure va después del último rescue y se ejecuta siempre al terminar el bloque, tanto si acaba bien como si sale por un error:
def leer_primera_linea(ruta)
fichero = File.open(ruta)
fichero.gets
rescue Errno::ENOENT
"no existe"
ensure
fichero&.close # &. evita el error si el fichero no llegó a abrirse
end
Para ficheros, en Ruby es más habitual pasar un bloque a File.open: lo cierra solo al terminar, incluso si hay un error.
primera = File.open("prueba.txt") { |fichero| fichero.gets }
Más sobre ficheros en Trabajar con ficheros en Ruby.
else: solo si no hubo error
Entre rescue y ensure puedes poner un else, que solo se ejecuta si no saltó ninguna excepción.
rescue o por else, nunca por los dos, y los dos caminos acaban en ensure; si ningún rescue encaja, tras el ensure el error sube.Reintentar: retry
Dentro de un rescue, retry vuelve a ejecutar el bloque begin desde el principio. Úsalo siempre con un límite, o puedes crear un bucle infinito. En este ejemplo se prueba el puerto 8000 y, si falla, el 8080; si fallan los dos, el error sube a quien llamó. conectar es de mentira, para que puedas ejecutarlo:
# Un conectar de mentira: el 8000 siempre está ocupado
def conectar(puerto)
raise IOError, "puerto #{puerto} ocupado" if puerto == 8000
"conectado al #{puerto}"
end
intentos = 0
puertos = [8000, 8080]
begin
puerto = puertos[intentos]
puts conectar(puerto)
rescue IOError => error
puts "Fallo: #{error.message}"
intentos += 1
retry if intentos < puertos.size
raise # sin argumentos, relanza el error actual
end
Fallo: puerto 8000 ocupado
conectado al 8080
Si los dos puertos estuvieran ocupados ([8000, 8000]), tras el segundo fallo el raise deja subir el error:
Fallo: puerto 8000 ocupado
Fallo: puerto 8000 ocupado
retry.rb:3:in 'Object#conectar': puerto 8000 ocupado (IOError)
Lanzar excepciones: raise
| Forma | Qué hace |
|---|---|
raise |
Relanza la excepción que se está tratando (o un RuntimeError si no hay ninguna) |
raise "Mala codificación" |
Lanza un RuntimeError con ese mensaje |
raise ArgumentError, "Edad negativa" |
Lanza una excepción de esa clase con ese mensaje |
raise ArgumentError, "Edad negativa", caller |
Igual, y el tercer argumento sustituye la traza |
Crear tus propias excepciones
Una excepción propia es una clase que hereda de StandardError. Le das un nombre que explica el problema y, si quieres, un mensaje por defecto:
class SaldoInsuficiente < StandardError
def initialize(saldo)
super("Saldo insuficiente: quedan #{saldo} €")
end
end
begin
raise SaldoInsuficiente.new(5)
rescue SaldoInsuficiente => error
puts error.message # Saldo insuficiente: quedan 5 €
end
Si tu programa tiene varios errores propios, dales una madre común. Así quien llama puede capturarlos todos de una vez, o solo uno concreto:
class ErrorDeTienda < StandardError; end
class SinStock < ErrorDeTienda; end
class PagoRechazado < ErrorDeTienda; end
begin
raise SinStock, "no quedan camisetas"
rescue ErrorDeTienda => error
puts "#{error.class}: #{error.message}" # SinStock: no quedan camisetas
end
Traducir un error sin perder el original: cause
A veces capturas un error de una librería y lanzas otro que tenga sentido en tu programa. Ruby guarda el original en cause, así que no pierdes la pista de qué pasó de verdad:
require "json"
def leer_precio(texto)
JSON.parse(texto).fetch("precio")
rescue JSON::ParserError
raise ArgumentError, "JSON no válido: #{texto}"
end
begin
leer_precio("{precio: 5")
rescue ArgumentError => error
puts error.message # JSON no válido: {precio: 5
puts error.cause.class # JSON::ParserError
end
rescue en una sola línea
Puedes poner rescue al final de una línea para dar un valor si algo falla:
edad = Integer("abc") rescue 0
p edad # 0
Es cómodo, pero captura cualquier StandardError, no solo el que esperabas, y no te deja ver cuál fue. Para convertir texto en número hay una opción mejor: Integer acepta exception: false y devuelve nil si no puede.
p Integer("abc", exception: false) # nil
p Integer("42", exception: false) # 42
Captura solo el error que sabes resolver.
catch y throw
No son excepciones, aunque se parezcan a las de otros lenguajes. Sirven para salir de varios bucles anidados de golpe. throw salta al catch con la misma etiqueta y le pasa un valor:
primero_mayor_que_dos = catch(:encontrado) do
[[1, 2], [3, 4]].each do |fila|
fila.each { |n| throw :encontrado, n if n > 2 }
end
nil
end
primero_mayor_que_dos # => 3
Para errores, usa raise y rescue; para salir de bucles, catch y throw.
Errores frecuentes
1. Capturar Exception. Parece lo más seguro, pero atrapa también lo que debe parar el programa, como exit o Ctrl + C:
begin
exit
rescue Exception
puts "He capturado hasta el exit"
end
puts "El programa sigue vivo"
He capturado hasta el exit
El programa sigue vivo
Captura StandardError como mucho y, mejor, la clase concreta.
2. Un rescue sin clase que esconde tus erratas. NoMethodError también es un StandardError, así que un rescue genérico se traga un método mal escrito:
def total(precios)
precios.sun
rescue
0
end
p total([10, 20]) # 0, y nadie se entera del error de escritura
Sin el rescue, Ruby te habría dicho undefined method ‘sun’ for an instance of Array en la primera ejecución. Captura solo el error que sabes resolver.
3. Poner primero el rescue más general. Ruby usa la primera cláusula que encaje, y StandardError encaja con casi todo. La de debajo nunca se ejecuta, y Ruby no avisa:
def dividir(a, b)
a / b
rescue StandardError
"error genérico"
rescue ZeroDivisionError
"no se puede dividir entre cero"
end
puts dividir(1, 0) # error genérico
4. Hacer que tus excepciones hereden de Exception. Un rescue sin clase no las captura, y el programa se para:
class PagoRechazado < Exception; end
begin
raise PagoRechazado, "tarjeta caducada"
rescue => error
puts "Capturado: #{error.message}"
end
pago.rb:4:in '<main>': tarjeta caducada (PagoRechazado)
Con class PagoRechazado < StandardError el mismo código escribe «Capturado: tarjeta caducada».
5. Lanzar un texto en vez de una clase. raise "no encuentro config.yml" lanza un RuntimeError, que no dice nada del problema. Quien quiera capturarlo tendrá que capturar todos los RuntimeError o comparar el mensaje. Una clase propia (ConfigNoEncontrada) o una de Ruby que encaje (ArgumentError, KeyError) le da un nombre al problema.
Compruébalo con un test
Que un método lance el error correcto también es comportamiento, y se prueba. En Minitest, assert_raises comprueba que el bloque lanza esa excepción y te la devuelve para revisar el mensaje:
require "minitest/autorun"
class SaldoInsuficiente < StandardError
def initialize(saldo)
super("Saldo insuficiente: quedan #{saldo} €")
end
end
class Cuenta
attr_reader :saldo
def initialize(saldo) = @saldo = saldo
def retirar(importe)
raise SaldoInsuficiente.new(saldo) if importe > saldo
@saldo -= importe
end
end
class CuentaTest < Minitest::Test
def test_retirar_mas_de_lo_que_hay_lanza_el_error
cuenta = Cuenta.new(5)
error = assert_raises(SaldoInsuficiente) { cuenta.retirar(20) }
assert_equal "Saldo insuficiente: quedan 5 €", error.message
assert_equal 5, cuenta.saldo
end
end
ruby cuenta_test.rb
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
El test comprueba tres cosas: que salta el error, que el mensaje dice cuánto queda y que, tras el error, el saldo sigue igual. Si alguien moviera la resta por encima del raise, el error seguiría saltando, pero la cuenta ya habría perdido el dinero. El test lo detecta:
CuentaTest#test_retirar_mas_de_lo_que_hay_lanza_el_error [cuenta_test.rb:26]:
--- expected
+++ actual
@@ -1 +1 @@
-"Saldo insuficiente: quedan 5 €"
+"Saldo insuficiente: quedan -15 €"
Para empezar con tests en Ruby, lee la introducción al testing en Ruby.
Cuándo no usar excepciones
Una excepción es para lo que no debería pasar y no se puede resolver ahí mismo: un fichero que falta, una conexión caída, un dato imposible. No es la herramienta para lo que pasa a menudo y es parte normal del programa:
- Un dato que el usuario escribe mal. Que alguien escriba «abc» en un campo de edad es de esperar.
Integer(texto, exception: false)devuelvenily lo tratas con unif, sinrescue. - Una clave que puede no estar.
hash.fetch(:clave, valor_por_defecto)ohash[:clave]dicen lo que quieres sin lanzar nada. Lo tienes en Hashes en Ruby. - Buscar algo que puede no existir.
finddevuelvenilsi no hay resultado. Lanzar un error por eso obliga a todos los que llaman a escribir unrescue. - Salir de un bucle. Para eso están
breako, en bucles anidados,catchythrow.
La regla práctica: si quien llama tiene que comprobar el caso cada vez, devuelve un valor; si el caso es raro y quien llama no puede hacer nada útil, lanza una excepción.
Ejercicio de cinco minutos
- Copia el test de
Cuentay pásalo. Después quita la línea que compara el mensaje, mueve la resta por encima delraisey comprueba que la última línea del test sigue detectando el fallo. - Añade un método
ingresar(importe)que lanceArgumentErrorsi el importe no es positivo. Escribe primero el test conassert_raises. - Cambia
SaldoInsuficiente < StandardErrorpor< Exceptiony prueba a capturarlo con unrescuesin clase. ¿Qué pasa?
Sigue leyendo
- Hashes en Ruby, con
fetchy suKeyError. - ¿Devolver null o lanzar una excepción?, con ejemplos en Java: cuándo un error merece una excepción y cuándo basta con decir que no hay resultado.
Fuentes
- Exceptions (un
rescuesin clase capturaStandardError, gana la primera cláusula que encaje,retryrepite todo elbegin,cause) y la claseException, documentación oficial de Ruby 4.0. - Kernel#Integer: con
exception: falsedevuelvenilen lugar de lanzar el error. - Los ejemplos del artículo original se ejecutaron con Ruby 4.0.9 en la imagen oficial
ruby:latestde Docker en noviembre de 2026, incluida la comprobación de la jerarquía conancestorsysuperclass. Se sustituyó el diagrama del original, escaneado de un libro, por el árbol en texto, y se corrigieron los ejemplos que usaban variables sin definir (socket,datos). - Los ejemplos nuevos (
retrycon unconectarde prueba, la familia de errores propios,cause, elrescueen línea, los errores frecuentes y el test) se han ejecutado el 11 de noviembre de 2026 en Docker con las imágenesruby:3.4(Ruby 3.4.12, Minitest 5.25.4) yruby:4.0(Ruby 4.0.9, Minitest 6.0.0), con la misma salida en las dos. Las salidas de error están recortadas a las líneas que importan.