Manejo de errores en Ruby y escritura de código robusto

Capítulo 6

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

Qué es una excepción y cuándo usarla

Una excepción es un objeto que representa una situación anómala que interrumpe el flujo normal del programa. Ruby las usa para señalar fallos (por ejemplo, archivo inexistente) o para comunicar reglas de negocio incumplidas (por ejemplo, “no hay stock”). Manejar excepciones no significa “ocultar” errores, sino controlar qué hacer cuando ocurre algo que puede pasar en producción.

Errores de programación vs. errores esperables

Una distinción clave para escribir código robusto es separar:

  • Errores de programación (bugs): nil inesperado, método mal escrito, índice fuera de rango por lógica incorrecta. Normalmente no se rescatan; se corrigen en el código. Ejemplos típicos: NoMethodError, NameError, ArgumentError por uso incorrecto de una API interna.
  • Errores esperables (operativos/entrada/sistema): el usuario escribe “abc” donde se esperaba un número, un archivo no existe, un servicio externo falla. Estos sí se manejan con rutas alternativas o mensajes claros. Ejemplos: Errno::ENOENT, JSON::ParserError, Timeout::Error, SocketError.

Regla práctica: rescata lo que puedas resolver o comunicar; no rescates lo que indica un bug que debe arreglarse.

begin / rescue / ensure: estructura básica

Ruby permite capturar excepciones con begin y rescue. El bloque ensure se ejecuta siempre, haya o no error, y es ideal para liberar recursos (cerrar archivos, conexiones, etc.).

begin  # 1) Código que puede fallar  contenido = File.read("datos.txt")  puts contenidorescue Errno::ENOENT => e  # 2) Qué hacer si el archivo no existe  warn "No se encontró el archivo: #{e.message}"rescue => e  # 3) Rescate genérico (úsalo con cuidado)  warn "Error inesperado: #{e.class} - #{e.message}"ensure  # 4) Se ejecuta siempre  puts "Intento de lectura finalizado"end

Buenas prácticas al rescatar

  • Rescata excepciones específicas antes que genéricas. Evita rescue => e si no tienes un plan claro.
  • No uses excepciones para control de flujo normal (por ejemplo, para decidir si un valor está vacío). Para eso, valida.
  • Incluye contexto en el manejo: qué operación falló, con qué parámetros, y qué alternativa se tomó.

raise: lanzar excepciones de forma intencional

raise se usa para señalar que una condición no se cumple. Es útil cuando detectas un estado inválido que no debería continuar.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

def aplicar_descuento(precio, porcentaje)  raise ArgumentError, "porcentaje debe estar entre 0 y 100" unless (0..100).include?(porcentaje)  precio - (precio * porcentaje / 100.0)end

También puedes relanzar la excepción original (por ejemplo, para agregar contexto) usando raise sin argumentos dentro de un rescue.

def cargar_config(path)  JSON.parse(File.read(path))rescue JSON::ParserError => e  raise "Config inválida en #{path}: #{e.message}"end

Mensajes de error útiles: qué deben incluir

Un mensaje de error útil reduce el tiempo de diagnóstico. Procura incluir:

  • Qué falló (operación).
  • Dónde (recurso/identificador: archivo, id, clave).
  • Por qué (regla o causa).
  • Qué hacer (si aplica: “verifica que el archivo exista”, “ingresa un número”).
MaloMejor
"Error""No se pudo leer 'clientes.csv': archivo inexistente. Verifica la ruta."
"Dato inválido""Cantidad inválida: 'abc'. Ingresa un entero mayor o igual a 0."

Validaciones y rutas alternativas sin duplicar lógica

Para evitar duplicación, separa en capas: parseo/validación (entrada), lógica de dominio (reglas), y adaptadores (archivos/red). Así puedes manejar errores en el borde sin repetir la lógica central.

Patrón 1: método “parse_” que valida y falla con mensaje claro

def parse_entero!(texto, campo:)  Integer(texto)rescue ArgumentError  raise ArgumentError, "#{campo} debe ser un entero. Recibido: #{texto.inspect}"end

Luego reutilizas ese método en distintos puntos sin reescribir el rescate.

Patrón 2: ruta alternativa con valor por defecto (cuando tenga sentido)

Para errores esperables, a veces conviene una alternativa segura.

def leer_texto_o_vacio(path)  File.read(path)rescue Errno::ENOENT  ""end

Úsalo solo si el valor por defecto no oculta un problema importante. Si el archivo es crítico, mejor lanzar una excepción con contexto.

Patrón 3: envolver excepciones externas en excepciones del dominio

Si tu mini-proyecto tiene reglas propias, conviene exponer errores del dominio en lugar de errores técnicos. Eso hace el código más claro para quien lo usa.

class PersistenciaError < StandardError; enddef guardar_pedido(path, data)  File.write(path, data)rescue SystemCallError => e  # Agrupa errores de sistema (permisos, disco, etc.)  raise PersistenciaError, "No se pudo guardar el pedido en #{path}: #{e.message}"end

Ejercicio 1: capturar errores de conversión numérica

Objetivo: pedir una cantidad, convertirla a entero y manejar entradas inválidas sin que el programa explote.

Paso a paso

  • Intenta convertir con Integer() (más estricto que to_i).
  • Rescata ArgumentError y muestra un mensaje accionable.
  • Reintenta o devuelve un valor controlado.
def pedir_entero(mensaje, min: nil)  loop do    print mensaje    input = STDIN.gets&.chomp    begin      n = Integer(input)      if min && n < min        puts "Debe ser >= #{min}."        next      end      return n    rescue ArgumentError      puts "Entrada inválida: #{input.inspect}. Ingresa un número entero."    end  endendcantidad = pedir_entero("Cantidad a comprar: ", min: 1)puts "Comprarás #{cantidad} unidades"

Nota: aquí el error es esperable (entrada del usuario), por eso se maneja con reintento.

Ejercicio 2: manejar archivos inexistentes

Objetivo: leer un archivo de configuración o datos. Si no existe, ofrecer una alternativa (crear uno, usar valores por defecto o abortar con mensaje claro).

Opción A: usar valores por defecto si falta

require "json"def cargar_config_o_default(path)  JSON.parse(File.read(path))rescue Errno::ENOENT  { "modo" => "demo" }rescue JSON::ParserError => e  raise "Config corrupta en #{path}: #{e.message}"endconfig = cargar_config_o_default("config.json")p config

Opción B: crear el archivo si no existe

require "json"def asegurar_config(path)  File.read(path)rescue Errno::ENOENT  default = { "modo" => "demo" }  File.write(path, JSON.pretty_generate(default))  JSON.pretty_generate(default)endputs asegurar_config("config.json")

Elige A o B según el caso: si el archivo es opcional, A; si es parte del setup, B puede mejorar la experiencia.

Ejercicio 3: excepciones personalizadas para reglas del dominio (StockInsuficienteError)

Objetivo: modelar una regla de negocio: no se puede vender más de lo disponible. En lugar de devolver false o nil (que obliga a revisar en todos lados), lanza una excepción del dominio con datos útiles.

Paso 1: definir la excepción

class StockInsuficienteError < StandardError  attr_reader :sku, :disponible, :solicitado  def initialize(sku:, disponible:, solicitado:)    @sku = sku    @disponible = disponible    @solicitado = solicitado    super("Stock insuficiente para #{sku}: disponible=#{disponible}, solicitado=#{solicitado}")  endend

Paso 2: usarla en una operación de dominio

def descontar_stock!(inventario, sku, cantidad)  disponible = inventario.fetch(sku, 0)  if cantidad > disponible    raise StockInsuficienteError.new(      sku: sku, disponible: disponible, solicitado: cantidad    )  end  inventario[sku] = disponible - cantidadendinventario = { "A1" => 3 }descontar_stock!(inventario, "A1", 2) # OKdescontar_stock!(inventario, "A1", 5) # Lanza excepción

Paso 3: capturarla en el borde (interfaz/entrada) y ofrecer alternativa

begin  descontar_stock!(inventario, "A1", 5)  puts "Compra realizada"rescue StockInsuficienteError => e  puts e.message  puts "Sugerencia: reduce la cantidad o elige otro producto."end

Observa la separación: la función de dominio descontar_stock! no imprime ni pregunta nada; solo aplica reglas. La interacción con el usuario ocurre al capturar la excepción.

Checklist rápido para código robusto con excepciones

  • Rescata solo lo que puedas manejar; deja que los bugs exploten en desarrollo.
  • Prefiere excepciones específicas y mensajes con contexto.
  • Usa ensure para limpieza garantizada.
  • Valida entrada antes de ejecutar lógica crítica; usa métodos de parseo reutilizables.
  • Crea excepciones del dominio para reglas de negocio y captura en los bordes del sistema.

Ahora responde el ejercicio sobre el contenido:

¿Cuál enfoque describe mejor una práctica robusta para manejar excepciones en Ruby?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un manejo robusto distingue entre errores esperables (entrada/sistema) que pueden comunicarse o tener alternativa, y bugs que deben corregirse. Por eso se prefieren rescates específicos y se evita usar excepciones como control de flujo normal.

Siguiente capítulo

Entrada y salida en Ruby: archivos, rutas y formatos comunes

Arrow Right Icon
Portada de libro electrónico gratuitaRuby desde Cero para principiantes: Programación Moderna, Clara y Eficiente
60%

Ruby desde Cero para principiantes: Programación Moderna, Clara y Eficiente

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.