Manejo básico de errores en Lua
Cuando desarrollamos programas, es inevitable que se produzcan errores. Estos pueden ser causados por múltiples factores: datos de entrada incorrectos, fallos en operaciones externas, errores lógicos en nuestro código, o situaciones inesperadas durante la ejecución. El manejo adecuado de errores es fundamental para crear aplicaciones robustas y confiables que puedan recuperarse de situaciones problemáticas o, al menos, informar al usuario de manera clara sobre qué ha fallado.
En Lua, el sistema de manejo de errores es simple pero efectivo. A diferencia de lenguajes como Java o Python que utilizan excepciones estructuradas con bloques try-catch, Lua emplea un enfoque basado en funciones que permite capturar y gestionar errores de forma flexible. Este sistema se integra de manera natural con la filosofía minimalista del lenguaje, proporcionando las herramientas necesarias sin añadir complejidad innecesaria.
En este artículo exploraremos los fundamentos del manejo de errores en Lua. Aprenderás a identificar los tipos comunes de errores, a lanzar errores manualmente cuando sea necesario, a proteger tu código contra fallos inesperados, y a implementar estrategias efectivas para que tus programas sean más resilientes y fáciles de depurar.
Tipos comunes de errores en Lua
Antes de aprender a manejar errores, es importante conocer qué tipos de errores podemos encontrar durante el desarrollo con Lua. Estos se pueden clasificar en cuatro categorías principales:
Errores de sintaxis
Los errores de sintaxis ocurren cuando el código no cumple con las reglas gramaticales del lenguaje. Estos errores impiden que el programa se ejecute, ya que el intérprete no puede entender lo que se intenta hacer. Son detectados en la fase de carga o compilación del script.
-- Error de sintaxis: falta 'then'
if a > 5
print("Mayor que 5")
end
-- Error de sintaxis: falta 'end'
function sumar(a, b)
return a + b
Cuando ejecutamos código con errores de sintaxis, Lua nos muestra mensajes claros indicando la línea y la naturaleza del problema:
lua: ejemplo.lua:2: 'then' expected near 'print'
Errores de tipo
Los errores de tipo suceden cuando intentamos realizar operaciones con datos de tipos incompatibles. Aunque Lua es un lenguaje de tipos dinámicos, no todas las operaciones están permitidas entre todos los tipos.
-- Error de tipo: intento de sumar un número con nil
local a = 5
local b = nil
print(a + b) -- Error: attempt to perform arithmetic on a nil value
-- Error de tipo: intento de llamar a una cadena como función
local texto = "hola"
texto() -- Error: attempt to call a string value
Errores de índice
Estos errores ocurren cuando intentamos acceder a elementos que no existen en una tabla, o cuando usamos índices de forma incorrecta.
-- Acceder a una clave inexistente devuelve nil, no genera error
local tabla = {a = 1, b = 2}
print(tabla.c) --> nil
-- Pero intentar indexar un valor nil sí genera error
local datos = nil
print(datos.campo) -- Error: attempt to index a nil value
-- Indexar con un valor inválido
local lista = {10, 20, 30}
print(lista[nil]) -- Error: table index is nil
Errores de lógica
Los errores de lógica son los más sutiles y difíciles de detectar. El código se ejecuta sin problemas técnicos, pero produce resultados incorrectos debido a fallos en el razonamiento del programador.
-- Error de lógica: condición invertida
function esPar(numero)
if numero % 2 == 1 then -- Debería ser == 0
return true
else
return false
end
end
print(esPar(4)) --> false (incorrecto)
print(esPar(5)) --> true (incorrecto)
Lanzamiento manual de errores
En ocasiones, queremos provocar un error deliberadamente cuando detectamos una situación anómala en nuestro programa. Lua proporciona dos funciones principales para este propósito: error() y assert().
Función error()
La función error() detiene la ejecución del programa y lanza un error con un mensaje personalizado. Su sintaxis es:
error(mensaje [, nivel])
El parámetro mensaje es el texto que se mostrará cuando ocurra el error. El parámetro opcional nivel indica en qué nivel de la pila de llamadas se debe reportar el error (por defecto es 1, que señala a la función que llamó a error()).
Veamos un ejemplo práctico:
function dividir(a, b)
if b == 0 then
error("No se puede dividir por cero")
end
return a / b
end
print(dividir(10, 2)) --> 5
print(dividir(10, 0)) -- Error: No se puede dividir por cero
El parámetro nivel es útil cuando queremos que el error se reporte en un contexto diferente:
function validarEdad(edad)
if type(edad) ~= "number" then
-- nivel 2: reporta el error en quien llamó a la función que llama a validarEdad
error("La edad debe ser un número", 2)
end
return edad
end
function procesarUsuario(nombre, edad)
validarEdad(edad)
print("Usuario: " .. nombre .. ", Edad: " .. edad)
end
procesarUsuario("Ana", "veinticinco")
-- El error señalará a procesarUsuario, no a validarEdad
Función assert()
La función assert() es una forma compacta de validar condiciones. Evalúa una expresión y, si es falsa o nil, lanza un error. Su sintaxis es:
assert(expresion [, mensaje])
Si expresion es true o cualquier valor diferente de nil/false, assert() devuelve todos los valores de la expresión. Si es false o nil, lanza un error con el mensaje especificado (o un mensaje genérico si no se proporciona).
function abrirArchivo(nombre)
local archivo = io.open(nombre, "r")
assert(archivo, "No se pudo abrir el archivo: " .. nombre)
return archivo
end
-- Si el archivo existe, todo funciona normalmente
local f = abrirArchivo("datos.txt")
-- Si no existe, se lanza un error descriptivo
local f2 = abrirArchivo("inexistente.txt")
-- Error: No se pudo abrir el archivo: inexistente.txt
La función assert() es especialmente útil para validaciones rápidas al inicio de funciones:
function calcularAreaRectangulo(ancho, alto)
assert(type(ancho) == "number", "El ancho debe ser un número")
assert(type(alto) == "number", "El alto debe ser un número")
assert(ancho > 0, "El ancho debe ser positivo")
assert(alto > 0, "El alto debe ser positivo")
return ancho * alto
end
print(calcularAreaRectangulo(5, 10)) --> 50
print(calcularAreaRectangulo(-5, 10)) -- Error: El ancho debe ser positivo
Captura de errores con pcall()
Cuando ejecutamos código que puede fallar, no siempre queremos que todo el programa se detenga. La función pcall() (protected call) nos permite ejecutar código de forma protegida, capturando cualquier error que pueda ocurrir.
La sintaxis de pcall() es:
estado, resultado = pcall(funcion, arg1, arg2, ...)
La función pcall() ejecuta la función especificada con los argumentos proporcionados y devuelve dos valores:
- estado: un valor booleano que indica si la ejecución fue exitosa (true) o si hubo un error (false)
- resultado: si
estadoes true, contiene el valor de retorno de la función; si es false, contiene el mensaje de error
Veamos un ejemplo básico:
function dividir(a, b)
if b == 0 then
error("División por cero no permitida")
end
return a / b
end
-- Llamada normal (sin protección)
-- print(dividir(10, 0)) -- Esto detendría el programa
-- Llamada protegida
local exito, resultado = pcall(dividir, 10, 2)
if exito then
print("Resultado: " .. resultado) --> Resultado: 5
else
print("Error: " .. resultado)
end
-- Otro intento con división por cero
local exito2, resultado2 = pcall(dividir, 10, 0)
if exito2 then
print("Resultado: " .. resultado2)
else
print("Error: " .. resultado2) --> Error: División por cero no permitida
end
Un ejemplo más completo de manejo de diferentes casos:
function procesarDatos(tabla, indice)
assert(type(tabla) == "table", "El primer argumento debe ser una tabla")
assert(type(indice) == "number", "El índice debe ser un número")
if tabla[indice] == nil then
error("El índice " .. indice .. " no existe en la tabla")
end
return tabla[indice] * 2
end
local datos = {10, 20, 30, 40, 50}
-- Caso exitoso
local ok, valor = pcall(procesarDatos, datos, 3)
if ok then
print("Valor procesado: " .. valor) --> Valor procesado: 60
else
print("Error al procesar: " .. valor)
end
-- Caso con índice inexistente
local ok2, valor2 = pcall(procesarDatos, datos, 10)
if ok2 then
print("Valor procesado: " .. valor2)
else
print("Error al procesar: " .. valor2)
--> Error al procesar: El índice 10 no existe en la tabla
end
-- Caso con argumento inválido
local ok3, valor3 = pcall(procesarDatos, "no es tabla", 1)
if ok3 then
print("Valor procesado: " .. valor3)
else
print("Error al procesar: " .. valor3)
--> Error al procesar: El primer argumento debe ser una tabla
end
Captura de errores con xpcall()
La función xpcall() (extended protected call) es similar a pcall(), pero permite especificar una función manejadora de errores que se ejecutará cuando ocurra un fallo. Esto es útil para obtener información adicional sobre el error, como el stack trace completo.
La sintaxis es:
estado, resultado = xpcall(funcion, manejadorError, arg1, arg2, ...)
El manejadorError es una función que recibe el mensaje de error como parámetro y puede procesarlo o ampliarlo antes de devolverlo.
function manejadorError(mensajeError)
print("\n=== Se produjo un error ===")
print("Mensaje: " .. mensajeError)
print("Stack trace:")
print(debug.traceback())
return mensajeError
end
function operacionRiesgosa(valor)
if valor < 0 then
error("El valor no puede ser negativo")
end
return math.sqrt(valor)
end
print("Inicio del programa")
local exito, resultado = xpcall(operacionRiesgosa, manejadorError, -5)
if exito then
print("Resultado: " .. resultado)
else
print("\nEl programa continúa después del error")
end
La salida mostrará información detallada sobre dónde ocurrió el error y cómo se llegó hasta ese punto, lo cual es invaluable durante el desarrollo y la depuración.
Interpretación de mensajes de error
Saber leer e interpretar los mensajes de error es una habilidad fundamental. Los mensajes de error de Lua siguen un formato estándar que nos proporciona información valiosa.
Un mensaje de error típico tiene esta estructura:
archivo.lua:numero_linea: descripcion_error
stack traceback:
archivo.lua:numero_linea: en contexto
...
Por ejemplo:
-- archivo: calculadora.lua
function dividir(a, b)
return a / b
end
function calcular()
local resultado = dividir(10, nil)
print(resultado)
end
calcular()
Este código producirá un error como:
calculadora.lua:2: attempt to perform arithmetic on a nil value (local 'b')
stack traceback:
calculadora.lua:2: in function 'dividir'
calculadora.lua:6: in function 'calcular'
calculadora.lua:9: in main chunk
Del mensaje de error podemos extraer:
- Archivo y línea:
calculadora.lua:2nos dice exactamente dónde está el problema - Descripción: "attempt to perform arithmetic on a nil value" explica qué salió mal
- Variable problemática: "(local 'b')" identifica la variable que causó el error
- Stack trace: muestra la secuencia de llamadas que llevó al error
Patrones básicos de manejo de errores
Existen varios patrones comunes para manejar errores de forma efectiva en Lua.
Validación de entrada
Siempre valida los parámetros al inicio de una función:
function crearUsuario(nombre, edad, email)
assert(type(nombre) == "string" and #nombre > 0,
"El nombre debe ser una cadena no vacía")
assert(type(edad) == "number" and edad >= 0 and edad <= 150,
"La edad debe ser un número entre 0 y 150")
assert(type(email) == "string" and email:match("@"),
"El email debe contener el símbolo @")
return {
nombre = nombre,
edad = edad,
email = email
}
end
-- Uso correcto
local usuario1 = crearUsuario("Carlos", 30, "carlos@ejemplo.com")
print(usuario1.nombre) --> Carlos
-- Uso incorrecto
local usuario2 = crearUsuario("", 30, "carlos@ejemplo.com")
-- Error: El nombre debe ser una cadena no vacía
Operaciones con archivos
Las operaciones con archivos son propensas a errores, por lo que siempre deben protegerse:
function leerArchivo(nombreArchivo)
local archivo, mensajeError = io.open(nombreArchivo, "r")
if not archivo then
return nil, "No se pudo abrir el archivo: " .. mensajeError
end
local contenido = archivo:read("*all")
archivo:close()
return contenido
end
-- Uso con manejo de errores
local contenido, error = leerArchivo("datos.txt")
if contenido then
print("Contenido del archivo:")
print(contenido)
else
print("Error: " .. error)
end
Retornar nil y mensaje de error
En lugar de usar error(), muchas funciones de Lua retornan nil seguido de un mensaje de error. Este patrón es más flexible:
function buscarUsuario(id)
local usuarios = {
[1] = {nombre = "Ana", edad = 25},
[2] = {nombre = "Luis", edad = 30}
}
if type(id) ~= "number" then
return nil, "El ID debe ser un número"
end
local usuario = usuarios[id]
if not usuario then
return nil, "Usuario no encontrado con ID: " .. id
end
return usuario
end
-- Uso del patrón
local usuario, error = buscarUsuario(1)
if usuario then
print("Usuario encontrado: " .. usuario.nombre)
else
print("Error: " .. error)
end
local usuario2, error2 = buscarUsuario(99)
if usuario2 then
print("Usuario encontrado: " .. usuario2.nombre)
else
print("Error: " .. error2) --> Error: Usuario no encontrado con ID: 99
end
Limpieza de recursos
Cuando trabajamos con recursos que deben liberarse (archivos, conexiones, etc.), es importante garantizar su limpieza incluso si ocurre un error:
function procesarArchivo(nombreArchivo)
local archivo = assert(io.open(nombreArchivo, "r"))
local exito, resultado = pcall(function()
local datos = archivo:read("*all")
-- Procesamiento que puede fallar
return datos:upper()
end)
archivo:close() -- Se ejecuta siempre, haya o no error
if not exito then
error("Error al procesar archivo: " .. resultado)
end
return resultado
end
Depuración básica
La depuración es el proceso de identificar y corregir errores. Lua proporciona herramientas simples pero efectivas para depurar código.
Uso de print() para depuración
El método más simple y efectivo es usar print() para mostrar valores intermedios:
function calcularPromedio(numeros)
print("DEBUG: Números recibidos:", #numeros) -- Depuración
local suma = 0
for i, valor in ipairs(numeros) do
print("DEBUG: Procesando índice", i, "valor", valor) -- Depuración
suma = suma + valor
end
print("DEBUG: Suma total:", suma) -- Depuración
local promedio = suma / #numeros
print("DEBUG: Promedio calculado:", promedio) -- Depuración
return promedio
end
local numeros = {10, 20, 30, 40, 50}
local resultado = calcularPromedio(numeros)
print("Resultado final:", resultado)
Función de depuración condicional
Para no llenar el código de prints, podemos crear una función de depuración que se active solo cuando sea necesario:
local DEBUG = true -- Cambiar a false para desactivar depuración
function debug_print(...)
if DEBUG then
print("[DEBUG]", ...)
end
end
function procesarDatos(datos)
debug_print("Iniciando procesamiento de datos")
debug_print("Número de elementos:", #datos)
local resultado = {}
for i, valor in ipairs(datos) do
debug_print("Procesando elemento", i, "con valor", valor)
resultado[i] = valor * 2
end
debug_print("Procesamiento completado")
return resultado
end
local datos = {1, 2, 3, 4, 5}
local resultado = procesarDatos(datos)
Inspección de tablas
Para depurar tablas complejas, es útil tener una función que las imprima de forma legible:
function imprimirTabla(t, nivel)
nivel = nivel or 0
local sangria = string.rep(" ", nivel)
for clave, valor in pairs(t) do
if type(valor) == "table" then
print(sangria .. clave .. " = {")
imprimirTabla(valor, nivel + 1)
print(sangria .. "}")
else
print(sangria .. clave .. " = " .. tostring(valor))
end
end
end
-- Ejemplo de uso
local configuracion = {
servidor = {
host = "localhost",
puerto = 8080,
opciones = {
timeout = 30,
reintentos = 3
}
},
debug = true
}
print("Configuración:")
imprimirTabla(configuracion)
Buenas prácticas
Para escribir código robusto y fácil de mantener, sigue estas recomendaciones:
Cuándo usar error() vs return nil
-
Usa
error()cuando:- El problema es tan grave que no tiene sentido continuar
- Es un error de programación que debe corregirse
- La función no puede cumplir su propósito de ninguna manera
-
Usa
return nil, mensajecuando:- El problema es esperado o recuperable
- La función es parte de una API pública
- El llamador puede tomar decisiones alternativas
-- error() para situaciones críticas
function dividir(a, b)
assert(type(a) == "number", "El dividendo debe ser un número")
assert(type(b) == "number", "El divisor debe ser un número")
if b == 0 then
error("División por cero")
end
return a / b
end
-- return nil para situaciones recuperables
function buscarEnCache(clave)
if type(clave) ~= "string" then
return nil, "La clave debe ser una cadena"
end
local valor = cache[clave]
if not valor then
return nil, "Clave no encontrada en caché"
end
return valor
end
Mensajes de error descriptivos
Los mensajes deben ser claros, informativos y útiles:
-- Mensaje pobre
function procesar(datos)
if not datos then
error("Error") -- ¿Qué error? ¿Por qué?
end
end
-- Mensaje descriptivo
function procesar(datos)
if not datos then
error("procesar(): se esperaba una tabla de datos, se recibió nil")
end
if type(datos) ~= "table" then
error("procesar(): se esperaba una tabla de datos, se recibió " .. type(datos))
end
end
No silenciar errores
Nunca captures un error y no hagas nada con él. Esto oculta problemas y dificulta la depuración:
-- MAL: silencia el error
local exito, resultado = pcall(operacionRiesgosa)
-- No hace nada con el error
-- BIEN: maneja o propaga el error
local exito, resultado = pcall(operacionRiesgosa)
if not exito then
print("Error en operación: " .. resultado)
-- O propaga el error:
error("Fallo al ejecutar operación riesgosa: " .. resultado)
end
Validación exhaustiva en funciones públicas
Las funciones que forman parte de una API pública deben validar todos sus parámetros:
function crearRectangulo(ancho, alto, color)
-- Validación exhaustiva
if type(ancho) ~= "number" then
error("crearRectangulo: el ancho debe ser un número, se recibió " .. type(ancho))
end
if ancho <= 0 then
error("crearRectangulo: el ancho debe ser positivo, se recibió " .. ancho)
end
if type(alto) ~= "number" then
error("crearRectangulo: el alto debe ser un número, se recibió " .. type(alto))
end
if alto <= 0 then
error("crearRectangulo: el alto debe ser positivo, se recibió " .. alto)
end
if color and type(color) ~= "string" then
error("crearRectangulo: el color debe ser una cadena, se recibió " .. type(color))
end
return {
ancho = ancho,
alto = alto,
color = color or "blanco"
}
end
Ejercicios prácticos
Para consolidar lo aprendido, intenta resolver estos ejercicios:
Ejercicio 1: Calculadora protegida
Crea una calculadora que maneje errores correctamente:
-- Implementa esta función
function calculadora(operacion, a, b)
-- Debe validar:
-- - Que 'operacion' sea una cadena válida: "+", "-", "*", "/"
-- - Que 'a' y 'b' sean números
-- - Que no se divida por cero
-- Retorna: resultado, nil en caso de éxito
-- Retorna: nil, mensaje_error en caso de fallo
end
-- Pruebas
print(calculadora("+", 5, 3)) -- Debe mostrar: 8
print(calculadora("/", 10, 0)) -- Debe mostrar: nil, mensaje de error
print(calculadora("^", 2, 3)) -- Debe mostrar: nil, mensaje de error
print(calculadora("*", "cinco", 3)) -- Debe mostrar: nil, mensaje de error
Ejercicio 2: Lector de configuración
Crea una función que lea un archivo de configuración y maneje todos los posibles errores:
-- Implementa esta función
function leerConfiguracion(nombreArchivo)
-- Debe manejar:
-- - Archivo no existe
-- - Archivo no se puede leer
-- - Contenido mal formado
-- Retorna: tabla_configuracion en caso de éxito
-- Retorna: nil, mensaje_error en caso de fallo
end
Ejercicio 3: Validador de formulario
Crea un validador que verifique datos de un formulario:
function validarFormulario(datos)
-- Debe validar que 'datos' contenga:
-- - nombre: cadena no vacía
-- - edad: número entre 0 y 120
-- - email: cadena que contenga "@"
-- - telefono: cadena de 9 dígitos (opcional)
--
-- Retorna: true si todo es válido
-- Lanza error con mensaje descriptivo si algo falla
end
Resumen
El manejo de errores es una parte fundamental de la programación profesional. En este artículo hemos aprendido los conceptos básicos que te permitirán escribir código Lua más robusto y confiable. Hemos visto cómo identificar los distintos tipos de errores que pueden ocurrir, desde errores de sintaxis hasta errores de lógica, y cómo cada uno requiere un enfoque diferente para su resolución.
Las funciones error() y assert() nos permiten lanzar errores cuando detectamos situaciones anómalas, comunicando claramente qué ha salido mal. Por otro lado, pcall() y xpcall() nos dan las herramientas para ejecutar código de forma protegida, capturando errores y permitiendo que nuestros programas se recuperen o al menos finalicen de forma elegante. La interpretación correcta de los mensajes de error y el stack trace es crucial para identificar rápidamente el origen de los problemas.
Finalmente, hemos explorado patrones comunes de manejo de errores y buenas prácticas que te ayudarán a escribir código más limpio y mantenible. Recuerda que un buen manejo de errores no solo hace que tus programas sean más robustos, sino que también facilita enormemente el proceso de depuración y mantenimiento. En el siguiente artículo del tutorial, exploraremos conceptos más avanzados que complementarán lo aprendido hasta ahora.