Errores en una corrutina: resume como pcall implícito, wrap y close
Un error dentro de una corrutina no se propaga: la mata y sale por el primer valor de retorno de resume, que actúa como un pcall permanente. coroutine.wrap invierte esa decisión y vuelve a lanzarlo. La traza se pierde salvo que la captures dentro, y coroutine.close existe para lo que quedó suspendido y nadie va a reanudar.
El manejo de errores es donde el modelo de corrutinas revela su carácter, porque obliga a decidir algo que en un programa de una sola pila ni se plantea: cuando falla un cómputo que se estaba ejecutando sobre otra pila, ¿de quién es el problema? Lua contesta con una frontera dura. resume nunca lanza nada, se comporta como un pcall que envuelve la corrutina entera y te devuelve el fallo como dato. Esa decisión es correcta y a la vez incómoda, y de la incomodidad nacen las tres cosas que trata esta lección: coroutine.wrap, que la revierte, la traza de pila que se evapora si no la capturas a tiempo, y coroutine.close, que se ocupa de lo que quedó a medias.
- Predecir qué devuelve
resumecuando el cuerpo de la corrutina lanza un error y en qué estado queda. - Comparar
createywrapen cuanto a valores de retorno, propagación de errores y limpieza. - Capturar una traza de pila útil desde dentro de la corrutina y saber por qué desde fuera no basta.
- Aplicar
coroutine.closea una corrutina suspendida que ya nadie va a reanudar y justificar cuándo hace falta.
resume nunca lanza: es un pcall implícito
Cuando la función de una corrutina lanza un error y nadie lo captura dentro, el error no atraviesa la frontera hacia quien reanudó. La corrutina muere y resume devuelve false seguido del objeto de error, exactamente con la misma forma que pcall.
local co = coroutine.create(function()
coroutine.yield("voy bien")
error({ codigo = 42, mensaje = "algo se torcio" })
end)
print(coroutine.resume(co)) --> true voy bien
local ok, err = coroutine.resume(co)
print(ok, type(err), err.codigo) --> false table 42
print(coroutine.status(co)) --> dead
Dos detalles importan. El primero es que el objeto de error viaja sin transformar: si lanzaste una tabla, recibes esa tabla; si lanzaste una cadena, recibes la cadena con la información de posición que error le añade por defecto, y resume no la envuelve ni le agrega nada. El segundo es que la corrutina queda muerta y no hay forma de continuar desde el punto del fallo: un error dentro de una corrutina no es recuperable desde fuera, solo diagnosticable. Si quieres que un fallo sea recuperable —seguir procesando el resto de los elementos, reintentar la operación— la captura tiene que estar dentro, con un pcall alrededor del tramo que puede fallar, y la corrutina seguirá viva porque su función nunca llegó a lanzar nada hacia arriba.
local co = coroutine.create(function(tareas)
for _, tarea in ipairs(tareas) do
local ok, resultado = pcall(tarea) -- se puede ceder dentro de pcall
coroutine.yield(ok, resultado) -- la corrutina sigue viva pase lo que pase
end
end)
Que se pueda ceder desde dentro de un pcall no es evidente ni fue siempre cierto. En Lua 5.1 la cesión no podía atravesar una función de C y ese código fallaba con attempt to yield across a C-call boundary; desde 5.2, pcall, xpcall, los metamétodos y el iterador del for genérico están implementados con continuaciones y dejan pasar la suspensión sin problemas.
Escribir coroutine.resume(co) sin recoger el resultado convierte cualquier fallo dentro de la corrutina en un no-evento absoluto: no se imprime nada, no se lanza nada, la corrutina simplemente deja de existir y el programa continúa como si tal cosa. Es el equivalente exacto de un pcall cuyo resultado se descarta, y en un sistema con muchas corrutinas produce fallos que tardan semanas en localizarse.
wrap propaga, create informa
coroutine.wrap recibe la misma función que create pero no devuelve un hilo: devuelve una función. Llamarla reanuda la corrutina con los argumentos que le pases, y sus valores de retorno son los de la cesión sin el booleano delante. Y si la corrutina falla, el error se relanza en el punto de la llamada.
local siguiente = coroutine.wrap(function()
coroutine.yield(1)
coroutine.yield(2)
error("se acabo la suerte")
end)
print(siguiente()) --> 1
print(siguiente()) --> 2
print(pcall(siguiente)) --> false n15-errores.lua:4: se acabo la suerte
La diferencia no es de comodidad sino de política de errores, y por eso la elección entre las dos no es cuestión de gusto.
| Aspecto | coroutine.create |
coroutine.wrap |
|---|---|---|
| Devuelve | Un hilo | Una función |
| Reanudar | coroutine.resume(co, ...) |
f(...) |
| Valores | true y los cedidos |
Solo los cedidos |
| Si falla | false y el error |
Relanza el error en el llamante |
| Si ya murió | false y el mensaje |
Lanza el error cannot resume dead coroutine |
| Acceso al hilo | Sí, para estado y cierre | No, queda encapsulado |
| Al fallar en 5.4 | La corrutina queda muerta | Se cierra además de morir |
flowchart TB E[Error dentro del cuerpo] --> Q[Como se creo la corrutina] Q -->|create| R[resume devuelve false y el objeto de error] Q -->|wrap| W[El error se relanza en el punto de la llamada] R --> S[El llamante decide si sigue o aborta] W --> T[Sube por la pila hasta el pcall mas cercano] W --> U[Ademas se cierran las variables pendientes]
La regla práctica se deduce de la tabla. Usa wrap cuando la corrutina sea un detalle de implementación de algo que se parece a una función: un iterador para un for genérico, un generador, un productor cuyo fallo debe sentirse como el fallo de quien lo consume. Usa create cuando la corrutina sea una entidad de primera clase que tu programa administra: una tarea en un planificador, una conexión, cualquier cosa que quieras poder consultar, contar, cerrar o guardar en una tabla. Un planificador escrito con wrap es un planificador que no puede distinguir entre una tarea que falló y una excepción de su propia lógica, y esa distinción es justamente su razón de ser.
Hay un matiz de la última fila que conviene no perder. En Lua 5.4, cuando la función de un wrap falla, la biblioteca cierra la corrutina antes de relanzar el error, de modo que sus variables de cierre pendientes se liberan. Con create no ocurre nada de eso: la corrutina queda muerta pero el cierre de sus recursos es cosa tuya.
La frontera es dura por diseño
Una corrutina no comparte pila, así que su error no tiene por dónde subir. resume lo convierte en dato porque es la única forma honesta de cruzarlo.
wrap es una máscara
Presenta la corrutina como una función corriente, y por eso adopta la política de errores de una función corriente: propagar.
El objeto de error viaja intacto
Tablas, valores numéricos u objetos de error propios cruzan sin conversión. wrap sí añade información de posición a los errores de tipo cadena.
Morir no es cerrar
Una corrutina muerta ha dejado de ejecutarse, pero sus ficheros abiertos y sus bloqueos siguen ahí hasta que alguien los suelte.
La traza perdida y cómo recuperarla
El objeto de error que recibes por resume no incluye traza de pila, y esto desconcierta a todo el mundo la primera vez. El motivo es estructural: la traza es un recorrido de una pila, y el error ocurrió en la pila de la corrutina, mientras que quien lo recibe está en otra. debug.traceback sin argumentos te dará la traza del llamante, que no tiene nada que ver con el sitio donde falló nada.
Hay dos remedios y el orden importa.
El primero, y el bueno, es capturar la traza dentro de la corrutina, en el instante del fallo, envolviendo el cuerpo con xpcall y un manejador que la genere. Es la única forma de obtener la información completa, incluidos los valores locales si usas la biblioteca de depuración.
local function cuerpo_seguro(f)
local manejador = function(e) return debug.traceback(tostring(e), 2) end
return function(...) return xpcall(f, manejador, ...) end
end
local co = coroutine.create(cuerpo_seguro(function()
local function hondo() error("fallo aqui") end
hondo()
end))
local ok, interno_ok, traza = coroutine.resume(co)
print(interno_ok, traza) -- traza completa del interior de la corrutina
Escribe ese envoltorio una sola vez y aplícalo en el único punto de tu programa donde se llama a create: el coste es una llamada extra por corrutina y el beneficio es que ninguna tarea puede fallar sin dejar rastro. El segundo remedio aprovecha un detalle de la implementación: en Lua 5.4 la pila de la corrutina no se deshace al fallar, así que aplicar debug.traceback al propio hilo justo después de un resume fallido devuelve todavía la traza del punto del error. Sirve para depurar en caliente, pero es frágil como política: depende de que nadie haya tocado el hilo entre medias y no te da nada si el error se detectó más tarde.
Cerrar lo que quedó a medias
Queda un caso que no cubre nada de lo anterior: la corrutina que ni terminó ni falló, sino que se quedó suspendida para siempre porque nadie volvió a reanudarla. Ocurre continuamente en la práctica: un consumidor que sale del bucle antes de agotar al productor, un for genérico interrumpido por un break, una tarea cancelada por el planificador.
Si esa corrutina tenía variables declaradas con el atributo <close> —un fichero, un bloqueo, una transacción—, su cierre está programado para ejecutarse al salir del bloque, y ese bloque nunca se abandona porque la ejecución sigue congelada dentro de él. Para eso existe coroutine.close.
local co = coroutine.create(function()
local f <close> = io.open("datos.txt", "r")
for linea in f:lines() do coroutine.yield(linea) end
end)
coroutine.resume(co) -- leo una linea
print(coroutine.close(co)) --> true cierra el fichero y mata la corrutina
print(coroutine.status(co)) --> dead
La función cierra todas las variables de cierre pendientes en orden inverso a su declaración y deja la corrutina muerta. Devuelve true si todo fue bien, o false y el objeto de error si algún metamétodo __close falló. Solo acepta corrutinas suspendidas o ya muertas: intentar cerrar la que está corriendo, o una que está en estado normal por haber reanudado a otra, produce un error, y con razón, porque en ambos casos su pila está en uso.
La regla de higiene se enuncia en una línea: toda corrutina que abandones antes de tiempo hay que cerrarla. Cerrar una corrutina que ya murió es legal y no cuesta nada, así que un planificador puede llamar a close sobre todas las tareas que retira sin comprobar en qué estado están.
En un programa de una sola pila, la propagación de errores es una consecuencia geométrica y no una decisión: el error sube porque hay un solo camino hacia arriba, y ese camino coincide exactamente con la cadena de llamadas que produjo el fallo, es decir, con la cadena de responsabilidades. Toda la intuición sobre excepciones —que suben hasta que alguien las entiende, que el que las captura es el que sabe qué hacer— descansa en esa coincidencia. Cuando aparece una segunda pila, la coincidencia se rompe, y se rompe de forma irreparable: quien reanuda una corrutina no es su llamante en ningún sentido significativo, porque no la escribió, no sabe qué está haciendo por dentro y puede reanudarla desde un sitio distinto en cada vuelta. Propagar el error hacia él sería mandarlo a alguien elegido por el azar de la planificación, y por eso resume no propaga: lo convierte en dato y te lo entrega para que decidas, que es lo único honesto que puede hacer. Ahora bien, esa honestidad tiene un precio que Lua no disimula y que conviene mirar de frente: la traza deja de reconstruirse sola, y con ella se va lo que en la práctica es más valioso que el propio error. La respuesta correcta no es echar de menos las excepciones sino aceptar la consecuencia completa del modelo —si el cómputo tiene su propia pila, la información sobre sus fallos hay que recogerla en su propia pila, en el instante en que ocurren, porque después ya no está—. Esto no es una peculiaridad de Lua ni de las corrutinas: es la misma disciplina que exige cualquier sistema donde el trabajo se desacopla de quien lo pidió, desde un planificador de tareas hasta una cola de mensajes. Las corrutinas simplemente son la versión más pequeña y honrada del problema, y por eso son el mejor sitio para aprenderlo.
Un error no capturado dentro de una corrutina la mata y sale por resume como false más el objeto de error, sin transformar y sin traza: resume es un pcall implícito y permanente. Para errores recuperables, captura dentro con pcall, que desde Lua 5.2 deja pasar la cesión. coroutine.wrap devuelve una función, omite el booleano, relanza el error en el llamante y en 5.4 cierra además la corrutina; úsalo para iteradores y create para tareas administradas. La traza solo se obtiene envolviendo el cuerpo con xpcall y debug.traceback. Y coroutine.close libera las variables de cierre de una corrutina suspendida que ya no vas a reanudar.
- Lanza una tabla como error desde dentro de una corrutina y comprueba que llega por
resumesin envolver. Repítelo con una cadena y observa la información de posición. - Escribe la misma corrutina con
createy conwrapy provoca un fallo en cada una. Anota qué hace falta escribir en cada caso para no perder el error. - Envuelve el cuerpo con
xpcallydebug.tracebacky compara la traza obtenida con la que devuelvedebug.tracebackaplicado al hilo desde fuera tras el fallo. - Abre un fichero como variable de cierre dentro de una corrutina, cede una vez y abandónala sin reanudar. Comprueba con el manejador si sigue abierto y después llama a
coroutine.close. - Intenta cerrar una corrutina desde dentro de ella misma y otra que esté en estado normal. Anota los dos mensajes de error y explica por qué el modelo no puede permitirlo.