Capítulo 17: Manejo de Errores
Capítulo 17: Manejo de Errores
El manejo de errores en Lua es fundamentalmente diferente al de lenguajes con sistema de excepciones tradicional. Lua utiliza un enfoque basado en protected calls y propagación explícita de errores que resulta eficiente y predecible.
1. error() y assert()
error(): Lanzar errores explícitos
La función error() termina la ejecución de la función actual y propaga un error:
function dividir(a, b)
if b == 0 then
error("División por cero no permitida")
end
return a / b
end
-- Esto detendrá el programa
-- dividir(10, 0) -- lua: script.lua:3: División por cero no permitida
Niveles de error para control del stack trace:
function validar_edad(edad)
if type(edad) ~= "number" then
-- Nivel 1 (default): reporta el error en esta función
error("Edad debe ser un número", 1)
end
if edad < 0 then
-- Nivel 2: reporta el error en la función que llamó a validar_edad
error("Edad no puede ser negativa", 2)
end
return edad
end
function procesar_usuario(edad)
validar_edad(edad) -- El error nivel 2 apuntará aquí
end
assert(): Validación concisa
assert() lanza un error si la condición es falsa:
function leer_archivo(nombre)
local archivo = assert(io.open(nombre, "r"), "No se pudo abrir: " .. nombre)
local contenido = archivo:read("*a")
archivo:close()
return contenido
end
-- Forma más verbosa equivalente:
function leer_archivo_verboso(nombre)
local archivo = io.open(nombre, "r")
if not archivo then
error("No se pudo abrir: " .. nombre)
end
local contenido = archivo:read("*a")
archivo:close()
return contenido
end
Patrón común: assert con funciones que retornan (valor, error)
function parse_json(texto)
local resultado, err = json.decode(texto)
assert(resultado, "JSON inválido: " .. (err or "desconocido"))
return resultado
end
2. pcall: Protected Calls
pcall (protected call) captura errores sin detener el programa:
function operacion_riesgosa()
error("¡Algo salió mal!")
end
local ok, resultado = pcall(operacion_riesgosa)
if ok then
print("Éxito:", resultado)
else
print("Error capturado:", resultado) -- resultado contiene el mensaje de error
end
-- Output: Error capturado: script.lua:2: ¡Algo salió mal!
Pasar argumentos a la función protegida
function dividir(a, b)
if b == 0 then
error("División por cero")
end
return a / b
end
local ok, resultado = pcall(dividir, 10, 2)
print(ok, resultado) -- true 5
local ok, resultado = pcall(dividir, 10, 0)
print(ok, resultado) -- false script.lua:3: División por cero
Envolver funciones para retornar (ok, result)
function safe_call(func, ...)
local ok, result_or_error = pcall(func, ...)
if ok then
return true, result_or_error
else
return false, result_or_error
end
end
-- Uso más idiomático:
local ok, resultado = safe_call(dividir, 10, 0)
if not ok then
print("Error:", resultado)
end
3. xpcall: Error Handler Custom
xpcall permite especificar un manejador de errores personalizado:
function error_handler(err)
return "ERROR INTERCEPTADO: " .. tostring(err)
end
function operacion()
local x = nil
return x.campo -- Esto causará un error
end
local ok, resultado = xpcall(operacion, error_handler)
print(ok) -- false
print(resultado) -- ERROR INTERCEPTADO: attempt to index a nil value
Agregar contexto al error
function enriquecer_error(err)
return {
mensaje = tostring(err),
timestamp = os.time(),
contexto = "Módulo de procesamiento"
}
end
local ok, resultado = xpcall(operacion_critica, enriquecer_error)
if not ok then
print("Error en:", resultado.contexto)
print("Mensaje:", resultado.mensaje)
print("Hora:", os.date("%c", resultado.timestamp))
end
4. debug.traceback
Obtener stack trace completo en errores:
function error_con_traceback(err)
return debug.traceback("Error: " .. tostring(err), 2)
end
function nivel3()
error("Fallo en nivel 3")
end
function nivel2()
nivel3()
end
function nivel1()
nivel2()
end
local ok, resultado = xpcall(nivel1, error_con_traceback)
if not ok then
print(resultado)
end
--[[ Output:
Error: script.lua:6: Fallo en nivel 3
stack traceback:
script.lua:2: in function 'error_con_traceback'
[C]: in function 'error'
script.lua:6: in function 'nivel3'
script.lua:10: in function 'nivel2'
script.lua:14: in function 'nivel1'
script.lua:17: in main chunk
]]
Traceback personalizado
function traceback_simplificado(err)
local trace = debug.traceback(err, 2)
-- Eliminar líneas internas de Lua
local lineas = {}
for linea in trace:gmatch("[^\n]+") do
if not linea:match("%[C%]") then -- Filtrar llamadas C
table.insert(lineas, linea)
end
end
return table.concat(lineas, "\n")
end
5. Propagación de Errores
Patrón (ok, err): Estilo Go
function leer_configuracion(archivo)
local file, err = io.open(archivo, "r")
if not file then
return nil, "No se pudo abrir archivo: " .. err
end
local contenido = file:read("*a")
file:close()
local config, parse_err = parse_json(contenido)
if not config then
return nil, "JSON inválido: " .. parse_err
end
return config, nil
end
-- Uso:
local config, err = leer_configuracion("config.json")
if err then
print("Error:", err)
return
end
print("Configuración cargada:", config.version)
Propagación con pcall
function procesar_datos(datos)
-- Validar
local ok, err = pcall(validar_esquema, datos)
if not ok then
return nil, "Validación falló: " .. err
end
-- Transformar
local transformados, err = transformar(datos)
if not transformados then
return nil, err
end
-- Guardar
local guardado, err = guardar_db(transformados)
if not guardado then
return nil, err
end
return transformados, nil
end
Error wrapping (envolver errores)
function wrap_error(operacion, err)
return string.format("[%s] %s", operacion, tostring(err))
end
function cargar_plugin(nombre)
local ok, plugin = pcall(require, nombre)
if not ok then
return nil, wrap_error("cargar_plugin", plugin)
end
local ok, err = pcall(plugin.inicializar)
if not ok then
return nil, wrap_error("inicializar_plugin", err)
end
return plugin, nil
end
6. Crear Jerarquía de Errores
Sistema de errores tipados
local ErrorTypes = {
VALIDACION = "ValidationError",
RED = "NetworkError",
PERMISO = "PermissionError",
NO_ENCONTRADO = "NotFoundError"
}
function crear_error(tipo, mensaje, detalles)
return {
tipo = tipo,
mensaje = mensaje,
detalles = detalles or {},
timestamp = os.time()
}
end
function es_error_de_tipo(err, tipo)
return type(err) == "table" and err.tipo == tipo
end
-- Uso:
function buscar_usuario(id)
if id < 0 then
error(crear_error(
ErrorTypes.VALIDACION,
"ID inválido",
{campo = "id", valor = id}
))
end
local usuario = db.usuarios[id]
if not usuario then
error(crear_error(
ErrorTypes.NO_ENCONTRADO,
"Usuario no existe",
{id = id}
))
end
return usuario
end
-- Manejo:
local ok, resultado = pcall(buscar_usuario, -5)
if not ok then
if es_error_de_tipo(resultado, ErrorTypes.VALIDACION) then
print("Error de validación en campo:", resultado.detalles.campo)
elseif es_error_de_tipo(resultado, ErrorTypes.NO_ENCONTRADO) then
print("Recurso no encontrado con ID:", resultado.detalles.id)
else
print("Error desconocido:", resultado.mensaje)
end
end
Factory de errores
local ErrorFactory = {}
function ErrorFactory.validacion(campo, mensaje)
return crear_error(ErrorTypes.VALIDACION, mensaje, {campo = campo})
end
function ErrorFactory.no_encontrado(recurso, id)
return crear_error(
ErrorTypes.NO_ENCONTRADO,
recurso .. " no encontrado",
{recurso = recurso, id = id}
)
end
function ErrorFactory.red(operacion, codigo)
return crear_error(
ErrorTypes.RED,
"Fallo en operación de red: " .. operacion,
{operacion = operacion, codigo = codigo}
)
end
-- Uso:
function validar_email(email)
if not email:match("@") then
error(ErrorFactory.validacion("email", "Formato de email inválido"))
end
end
7. Patrón (ok, result) vs Exceptions
Comparación de enfoques
Enfoque con pcall (estilo exception):
function procesar_pedido(pedido)
local ok, err = pcall(function()
validar_pedido(pedido)
procesar_pago(pedido)
actualizar_inventario(pedido)
enviar_confirmacion(pedido)
end)
if not ok then
log_error(err)
return false
end
return true
end
Enfoque con (ok, err) explícito (estilo Go):
function procesar_pedido(pedido)
local ok, err = validar_pedido(pedido)
if err then
log_error(err)
return nil, err
end
local pago, err = procesar_pago(pedido)
if err then
log_error(err)
return nil, err
end
local inventario, err = actualizar_inventario(pedido)
if err then
log_error(err)
revertir_pago(pago) -- Rollback
return nil, err
end
local ok, err = enviar_confirmacion(pedido)
if err then
log_error(err)
-- Continuar de todos modos
end
return pedido, nil
end
Ventajas y desventajas
| Aspecto | pcall/xpcall | (ok, err) explícito |
|---|---|---|
| Verbosidad | Menos código | Más verboso |
| Control flujo | Automático (stack unwinding) | Manual explícito |
| Performance | Overhead mínimo | Más eficiente |
| Rollback | Difícil | Fácil de implementar |
| Debugging | Stack trace completo | Contexto explícito |
8. DEEP DIVE: Stack Unwinding
Cómo funciona la propagación de errores
Cuando se llama a error(), Lua realiza “stack unwinding”:
function mostrar_stack_unwinding()
local function cleanup_nivel3()
print("Cleanup nivel 3 ejecutado")
end
local function nivel3()
-- Este cleanup NO se ejecutará si hay error
-- Lua no tiene finally o defer
cleanup_nivel3()
error("Fallo en nivel 3")
end
local function nivel2()
print("Entrando a nivel 2")
nivel3()
print("Esta línea NO se ejecuta")
end
local function nivel1()
print("Entrando a nivel 1")
nivel2()
print("Esta línea NO se ejecuta")
end
local ok, err = pcall(nivel1)
print("Error capturado:", err)
end
mostrar_stack_unwinding()
--[[
Entrando a nivel 1
Entrando a nivel 2
Cleanup nivel 3 ejecutado
Error capturado: script.lua:9: Fallo en nivel 3
]]
Simular RAII/Finally con xpcall
function with_resource(crear, destruir, callback)
local recurso = crear()
local function cleanup_handler(err)
destruir(recurso)
return err
end
local ok, resultado = xpcall(
function() return callback(recurso) end,
cleanup_handler
)
if ok then
destruir(recurso)
return resultado
else
error(resultado)
end
end
-- Uso:
with_resource(
function()
print("Abriendo archivo")
return io.open("datos.txt", "r")
end,
function(archivo)
print("Cerrando archivo")
archivo:close()
end,
function(archivo)
local contenido = archivo:read("*a")
error("Simulando error después de leer") -- El archivo SE cerrará
return contenido
end
)
Metatables y propagación de errores
local FileWrapper = {}
FileWrapper.__index = FileWrapper
function FileWrapper.new(nombre)
local self = setmetatable({}, FileWrapper)
local file, err = io.open(nombre, "r")
if not file then
error(err)
end
self.file = file
return self
end
function FileWrapper:read()
return self.file:read("*a")
end
function FileWrapper:__gc()
-- Garantizado al recolectarse
print("Cerrando archivo en __gc")
if self.file then
self.file:close()
end
end
-- El archivo se cerrará incluso con error
local ok, err = pcall(function()
local fw = FileWrapper.new("datos.txt")
local contenido = fw:read()
error("Error simulado")
end)
collectgarbage("collect") -- Forzar GC para ver el __gc
9. SOAPBOX: Errors vs Exceptions
La filosofía de Lua sobre errores
Lua deliberadamente eligió no tener un sistema de excepciones tradicional (try/catch). En su lugar, ofrece herramientas simples y composables.
Ventajas del enfoque Lua:
- Explicititud: El flujo de control es siempre visible
- Performance: Sin overhead de stack unwinding automático
- Simplicidad: Menos construcciones del lenguaje
- Composabilidad: pcall/xpcall son funciones normales
Desventajas:
- Sin garantía de cleanup: No hay finally/defer nativo
- Verboso: Código de manejo de errores puede repetirse
- Curva de aprendizaje: Programadores esperan try/catch
Cuándo usar cada patrón
Usa pcall/xpcall cuando:
- Llamas a código externo no confiable
- Necesitas capturar errores de bibliotecas C
- El error es inesperado y catastrófico
- Necesitas stack trace completo
Usa (ok, err) cuando:
- El “error” es parte del flujo normal (archivo no existe)
- Necesitas rollback/compensación
- Quieres máximo control sobre el flujo
- Performance es crítica
-- pcall: Error inesperado
local ok, plugin = pcall(require, "plugin_terceros")
if not ok then
log_fatal("No se pudo cargar plugin:", plugin)
os.exit(1)
end
-- (ok, err): Flujo esperado
local archivo, err = io.open("config.json", "r")
if not archivo then
print("Config no encontrado, usando defaults")
usar_configuracion_default()
else
cargar_configuracion(archivo)
archivo:close()
end
10. Caso Práctico: Validador de Datos con Errores Detallados
Sistema completo de validación
-- 1. Tipos de error
local ValidationError = {tipo = "ValidationError"}
ValidationError.__index = ValidationError
function ValidationError.new(campo, mensaje, valor)
local self = setmetatable({}, ValidationError)
self.campo = campo
self.mensaje = mensaje
self.valor = valor
self.timestamp = os.time()
return self
end
function ValidationError:__tostring()
return string.format(
"[ValidationError] Campo '%s': %s (valor: %s)",
self.campo,
self.mensaje,
tostring(self.valor)
)
end
-- 2. Validadores básicos
local Validators = {}
function Validators.required(valor, campo)
if valor == nil or valor == "" then
error(ValidationError.new(
campo,
"Campo requerido",
valor
))
end
return valor
end
function Validators.min_length(minimo)
return function(valor, campo)
if type(valor) == "string" and #valor < minimo then
error(ValidationError.new(
campo,
string.format("Mínimo %d caracteres", minimo),
valor
))
end
return valor
end
end
function Validators.email(valor, campo)
if not valor:match("^[%w._%+-]+@[%w.-]+%.[%a]+$") then
error(ValidationError.new(
campo,
"Formato de email inválido",
valor
))
end
return valor
end
function Validators.range(min, max)
return function(valor, campo)
if type(valor) ~= "number" or valor < min or valor > max then
error(ValidationError.new(
campo,
string.format("Debe estar entre %d y %d", min, max),
valor
))
end
return valor
end
end
-- 3. Motor de validación
local Schema = {}
Schema.__index = Schema
function Schema.new(definicion)
local self = setmetatable({}, Schema)
self.campos = definicion
return self
end
function Schema:validate(datos)
local errores = {}
local resultado = {}
for campo, validadores in pairs(self.campos) do
local valor = datos[campo]
local ok, err = pcall(function()
for _, validador in ipairs(validadores) do
valor = validador(valor, campo)
end
end)
if ok then
resultado[campo] = valor
else
if getmetatable(err) == ValidationError then
table.insert(errores, err)
else
-- Error inesperado
table.insert(errores, ValidationError.new(
campo,
"Error de validación: " .. tostring(err),
valor
))
end
end
end
if #errores > 0 then
return nil, errores
end
return resultado, nil
end
-- 4. Uso del sistema
local schema_usuario = Schema.new({
nombre = {
Validators.required,
Validators.min_length(3)
},
email = {
Validators.required,
Validators.email
},
edad = {
Validators.required,
Validators.range(18, 100)
}
})
-- Validar datos
local datos_usuario = {
nombre = "Jo", -- Muy corto
email = "invalido", -- No es email
edad = 150 -- Fuera de rango
}
local usuario, errores = schema_usuario:validate(datos_usuario)
if errores then
print("Errores de validación encontrados:")
for _, err in ipairs(errores) do
print(" -", tostring(err))
end
else
print("Usuario validado:", usuario.nombre, usuario.email)
end
--[[
Errores de validación encontrados:
- [ValidationError] Campo 'nombre': Mínimo 3 caracteres (valor: Jo)
- [ValidationError] Campo 'email': Formato de email inválido (valor: invalido)
- [ValidationError] Campo 'edad': Debe estar entre 18 y 100 (valor: 150)
]]
Validación con transformación
-- Validadores que transforman
function Validators.trim(valor, campo)
if type(valor) == "string" then
return valor:match("^%s*(.-)%s*$")
end
return valor
end
function Validators.uppercase(valor, campo)
if type(valor) == "string" then
return valor:upper()
end
return valor
end
function Validators.to_number(valor, campo)
local num = tonumber(valor)
if not num then
error(ValidationError.new(campo, "No es un número válido", valor))
end
return num
end
-- Schema con transformación
local schema_producto = Schema.new({
codigo = {
Validators.required,
Validators.trim,
Validators.uppercase
},
precio = {
Validators.required,
Validators.to_number,
Validators.range(0, 999999)
}
})
local producto, err = schema_producto:validate({
codigo = " abc123 ",
precio = "29.99"
})
if producto then
print("Código:", producto.codigo) -- ABC123
print("Precio:", producto.precio) -- 29.99 (number)
end
11. Ejercicios
Ejercicio 1: Sistema de Retry con Backoff
Implementa una función que reintente operaciones fallidas con backoff exponencial:
--[[
retry(funcion, intentos_maximos, backoff_inicial) -> resultado, error
Debe:
1. Reintentar la función hasta intentos_maximos veces
2. Usar backoff exponencial: esperar 1s, 2s, 4s, 8s...
3. Retornar (resultado, nil) si tiene éxito
4. Retornar (nil, ultimo_error) si falla todos los intentos
5. Loguear cada intento y error
Ejemplo de uso:
local resultado, err = retry(
function() return hacer_peticion_http() end,
5,
1 -- Empezar con 1 segundo
)
]]
function retry(func, max_intentos, backoff_inicial)
-- Tu código aquí
end
-- Prueba:
local intentos = 0
local resultado, err = retry(
function()
intentos = intentos + 1
if intentos < 3 then
error("Intento " .. intentos .. " falló")
end
return "¡Éxito en intento " .. intentos .. "!"
end,
5,
0.1
)
print(resultado or err)
Ejercicio 2: Circuit Breaker
Implementa el patrón Circuit Breaker para prevenir llamadas a servicios fallidos:
--[[
CircuitBreaker:
- Estados: CLOSED (normal), OPEN (bloqueado), HALF_OPEN (probando)
- Si falla threshold veces consecutivas, abre el circuito
- Después de timeout segundos, pasa a HALF_OPEN
- En HALF_OPEN, permite 1 intento: éxito -> CLOSED, fallo -> OPEN
Interfaz:
cb = CircuitBreaker.new(threshold, timeout)
resultado, err = cb:call(funcion)
estado = cb:get_estado()
]]
local CircuitBreaker = {}
CircuitBreaker.__index = CircuitBreaker
function CircuitBreaker.new(threshold, timeout)
-- Tu código aquí
end
function CircuitBreaker:call(func)
-- Tu código aquí
end
function CircuitBreaker:get_estado()
-- Tu código aquí
end
-- Prueba:
local cb = CircuitBreaker.new(3, 2) -- 3 fallos, 2 segundos timeout
for i = 1, 10 do
local ok, err = cb:call(function()
if math.random() > 0.7 then
return "OK"
else
error("Servicio falló")
end
end)
print(string.format(
"Intento %d: %s (Estado: %s)",
i,
ok or err,
cb:get_estado()
))
os.execute("sleep 0.5")
end
Ejercicio 3: Error Aggregator
Crea un sistema que acumule múltiples errores antes de fallar:
--[[
ErrorAggregator:
- Acumula errores en vez de fallar inmediatamente
- Permite continuar validaciones incluso con errores
- Retorna todos los errores al final
Interfaz:
agg = ErrorAggregator.new()
agg:try(funcion, contexto) -- Ejecuta y captura errores
tiene_errores = agg:has_errors()
errores = agg:get_errors()
resultado = agg:throw_if_errors() -- Lanza si hay errores
]]
local ErrorAggregator = {}
ErrorAggregator.__index = ErrorAggregator
function ErrorAggregator.new()
-- Tu código aquí
end
function ErrorAggregator:try(func, contexto)
-- Tu código aquí
end
function ErrorAggregator:has_errors()
-- Tu código aquí
end
function ErrorAggregator:get_errors()
-- Tu código aquí
end
function ErrorAggregator:throw_if_errors()
-- Tu código aquí
end
-- Prueba:
local agg = ErrorAggregator.new()
agg:try(function()
validar_nombre("A") -- Falla
end, "validacion_nombre")
agg:try(function()
validar_email("invalido") -- Falla
end, "validacion_email")
agg:try(function()
validar_edad(200) -- Falla
end, "validacion_edad")
if agg:has_errors() then
print("Errores encontrados:")
for _, err in ipairs(agg:get_errors()) do
print(string.format(" [%s] %s", err.contexto, err.mensaje))
end
end
Recursos Adicionales
- Programming in Lua - Error Handling
- Lua Users Wiki - Error Handling
- Roberto Ierusalimschy - Errors in Lua
Próximo capítulo: Módulos y Packages - Sistema de módulos, require, package.path, y creación de bibliotecas reutilizables.