| name | Lua Coroutines |
| description | Use when lua coroutines for cooperative multitasking including coroutine creation, yielding and resuming, passing values, generators, iterators, asynchronous patterns, state machines, and producer-consumer implementations. |
| allowed-tools | [] |
Lua Coroutines
Introduction
Coroutines in Lua provide cooperative multitasking, enabling functions to
suspend and resume execution. Unlike threads, coroutines don't run in parallel
but yield control explicitly, making them simpler to reason about while enabling
powerful asynchronous patterns without callback complexity.
Coroutines are first-class values in Lua, created from functions and managed
through the coroutine library. They maintain their own stack, local variables,
and instruction pointer, allowing suspension at any point and resumption later.
This enables elegant implementations of generators, iterators, and state machines.
This skill covers coroutine basics, yielding and resuming with values, generators
and iterators, producer-consumer patterns, asynchronous I/O simulation, state
machines, error handling, and practical coroutine patterns.
Coroutine Fundamentals
Coroutines enable functions to pause and resume execution, providing cooperative
multitasking without thread complexity.
local function simple_task()
print("Task started")
coroutine.yield()
print("Task resumed")
coroutine.yield()
print("Task finished")
end
local co = coroutine.create(simple_task)
print(coroutine.status(co))
coroutine.resume(co)
print(coroutine.status(co))
coroutine.resume(co)
coroutine.resume(co)
print(coroutine.status(co))
local function greet(name)
print("Hello, " .. name)
local response = coroutine.yield("What's your age?")
print(name .. " is " .. response .. " years old")
end
local co2 = coroutine.create(greet)
local success, question = coroutine.resume(co2, "Alice")
print(question)
coroutine.resume(co2, 30)
local function counter()
for i = 1, 5 do
coroutine.yield(i)
end
return "done"
end
local co3 = coroutine.create(counter)
repeat
local success, value = coroutine.resume(co3)
print(value)
until coroutine.status(co3) == "dead"
local function wrapped_task()
for i = 1, 3 do
coroutine.yield(i * 10)
end
end
local f = coroutine.wrap(wrapped_task)
print(f())
print(f())
print(f())
local function self_aware()
if coroutine.running() then
print("Running in coroutine")
else
print("Running in main")
end
end
self_aware()
coroutine.resume(coroutine.create(self_aware))
local function inner()
print("Inner start")
coroutine.yield("from inner")
print("Inner end")
end
local function outer()
print("Outer start")
inner()
print("Outer end")
end
local co4 = coroutine.create(outer)
coroutine.resume(co4)
coroutine.resume(co4)
local function echo()
while true do
local value = coroutine.yield()
if value == nil then break end
print("Echo: " .. value)
end
end
local co5 = coroutine.create(echo)
coroutine.resume(co5)
coroutine.resume(co5, "Hello")
coroutine.resume(co5, "World")
coroutine.resume(co5)
local function faulty()
print("Before error")
error("Something went wrong")
print("After error")
end
local co6 = coroutine.create(faulty)
local success, err = coroutine.resume(co6)
if not success then
print("Error caught: " .. err)
end
Coroutines enable cooperative multitasking where functions explicitly yield
control rather than being preempted.
Generators and Iterators
Coroutines elegantly implement generators and custom iterators for lazy
evaluation and infinite sequences.
local function range(from, to, step)
step = step or 1
return coroutine.wrap(function()
for i = from, to, step do
coroutine.yield(i)
end
end)
end
for n in range(1, 10, 2) do
print(n)
end
local function naturals()
return coroutine.wrap(function()
local n = 1
while true do
coroutine.yield(n)
n = n + 1
end
end)
end
local gen = naturals()
print(gen())
print(gen())
(gen())
.(
a, b = ,
.(a)
a, b = b, a + b
)
fib = fibonacci()
i = ,
(fib())
.(
value gen
predicate(value)
.(value)
)
evens = filter(range(, ), n % == )
n evens
(n)
.(
value gen
.(transform(value))
)
squared = map(range(, ), n * n )
n squared
(n)
.(
count =
value gen
count >= n
.(value)
count = count +
)
first5 = take(naturals(), )
n first5
(n)
generators = {...}
.(
_, gen (generators)
value gen
.(value)
)
combined = chain(range(, ), range(, ))
n combined
(n)
.(
v1 = gen1()
v2 = gen2()
v1 == v2 ==
.(v1, v2)
)
letters = .(
c .(, )
.(c)
)
zipped = zip(range(, ), letters)
num, letter zipped
(num, letter)
.(
n = n #arr
n ==
.(arr)
i = , n
arr[n], arr[i] = arr[i], arr[n]
perm(arr, n - )
arr[n], arr[i] = arr[i], arr[n]
copy = {}
i, v (array)
copy[i] = v
perm(copy)
)
perm permute({, , })
(.(perm, ))
.(
file = .(filename, )
file
line file:()
.(line)
file:()
)
Generators enable lazy evaluation and infinite sequences with clean, readable
syntax.
Producer-Consumer Pattern
Coroutines elegantly implement producer-consumer patterns without explicit
queues or callbacks.
local function producer()
return coroutine.create(function()
for i = 1, 10 do
print("Producing " .. i)
coroutine.yield(i)
end
end)
end
local function consumer(prod)
while coroutine.status(prod) ~= "dead" do
local success, value = coroutine.resume(prod)
if success and value then
print("Consuming " .. value)
end
end
end
local prod = producer()
consumer(prod)
local function filtered_producer(filter_fn)
return coroutine.create(function
i = ,
filter_fn(i)
.(i)
)
even_prod = filtered_producer( n % == )
consumer(even_prod)
consumers = {}
i = , num_consumers
consumers[i] = .(
success, value = .(prod)
success value ==
(.(, i, value))
.()
)
active =
active
active =
_, consumer (consumers)
.(consumer) ~=
.(consumer)
active =
stages = {...}
current =
_, stage (stages)
co = .(stage)
results = {}
value current
success, result = .(co, value)
success result
.(results, result)
current = .(
_, v (results)
.(v)
)
current
.(n * )
.(n + )
process = pipeline(double, add_ten)
result = process(range(, ))
n result
(n)
Scheduler = {}
{
tasks = {},
current =
}
.(scheduler.tasks, {
priority = priority,
= .(task_fn)
})
.(scheduler.tasks,
a.priority > b.priority
)
#scheduler.tasks >
task = scheduler.tasks[]
success, result = .(task.)
.(task.) ==
.(scheduler.tasks, )
.(scheduler.tasks, )
.(scheduler.tasks, task)
sched = Scheduler.new()
Scheduler.add(sched, ,
i = ,
( .. i)
.()
)
Scheduler.add(sched, ,
i = ,
( .. i)
.()
)
Scheduler.run(sched)
Producer-consumer patterns with coroutines eliminate callback complexity and
provide clear data flow.
Asynchronous Patterns
Coroutines enable asynchronous I/O patterns without callbacks, providing
sequential-looking code for async operations.
local Async = {}
function Async.sleep(seconds)
local wake_time = os.time() + seconds
coroutine.yield(wake_time)
end
function Async.run(tasks)
local waiting = {}
for _, task_fn in ipairs(tasks) do
local co = coroutine.create(task_fn)
table.insert(waiting, {coroutine = co, wake_time = 0})
end
while #waiting > 0 do
local current_time = os.time()
local still_waiting = {}
for _, task in ipairs(waiting) do
if current_time >= task.wake_time then
local success, wake_time = coroutine.resume(task.coroutine)
if coroutine.status(task.coroutine) ~=
.(still_waiting, {
= task.,
wake_time = wake_time
})
.(still_waiting, task)
waiting = still_waiting
#waiting >
.()
Async.run({
()
Async.sleep()
()
Async.sleep()
()
,
()
Async.sleep()
()
})
.( .. url)
.. url
urls = {
,
,
}
results = {}
_, url (urls)
response = http_get(url)
.(results, response)
results
Promise = {}
Promise. = Promise
= ({
state = ,
value = ,
callbacks = {}
}, Promise)
.state ==
.state =
.value = value
_, callback (.callbacks)
callback(value)
.(.(
executor(resolve)
))
.state ==
callback(.value)
.(.callbacks, callback)
p = Promise.new(
.()
resolve()
)
p:andThen(
( .. value)
)
args = {...}
.(
fn(.(args))
)
success, result = .(co)
result
fetch_user = async(
( .. id)
.()
{id = id, name = .. id}
)
main = async(
user = await(fetch_user())
( .. user.name)
)
.(main())
Async patterns with coroutines provide sequential code style for asynchronous
operations without callback nesting.
State Machines
Coroutines naturally implement state machines with clean state transitions and
local state preservation.
local function connection_state_machine()
local state = "disconnected"
return coroutine.wrap(function()
while true do
local event = coroutine.yield(state)
if state == "disconnected" then
if event == "connect" then
print("Connecting...")
state = "connecting"
end
elseif state == "connecting" then
if event == "connected" then
print("Connected!")
state = "connected"
elseif event == "error" then
print("Connection failed")
state = "disconnected"
end
elseif state == "connected" then
if event == "disconnect" then
()
state =
event ==
()
state ==
event ==
()
state =
)
conn = connection_state_machine()
(conn())
(conn())
(conn())
(conn())
(conn())
.(
chars = {}
escaped =
= .()
== escaped
== escaped
escaped =
.(chars, )
escaped =
.(chars)
)
health =
target =
.(
state =
health >
= .(state)
state ==
.event ==
target = .player
state =
state ==
.event ==
state =
.event ==
target =
state =
state ==
.event ==
()
.event ==
state =
.event ==
health = health - .damage
health <
state =
state ==
.event ==
state =
)
.(
.()
.()
.()
)
light = traffic_light()
i = ,
(light())
.(
current = tree.start
current
node = tree.nodes[current]
.(node.text, node.choices)
choice = .()
node.choices node.choices[choice]
current = node.choices[choice].
current =
)
dialog = dialog_tree({
start = ,
nodes = {
greeting = {
text = ,
choices = {
{text = , = },
{text = , = }
}
},
ask_quest = {
text = ,
choices = {
{text = , = },
{text = , = }
}
},
give_quest = {
text = ,
choices = {}
}
}
})
State machines with coroutines maintain state naturally without complex state
tracking structures.
Best Practices
-
Use coroutine.wrap for iterators as it provides simpler interface without
status checking
-
Check coroutine.resume return values to handle errors and detect
completion
-
Avoid yielding across C boundaries as it's not supported in standard Lua
-
Pass data through yield and resume rather than using global or upvalue
variables
-
Use coroutine.status to check if coroutine is dead before resuming
-
Create generators with coroutine.wrap for clean iteration syntax
-
Implement proper cleanup in coroutines using pcall for error handling
-
Avoid nested coroutine.resume calls as they complicate control flow
-
Use coroutine.running to check execution context and avoid invalid yields
-
Document yield points clearly to help readers understand suspension
points
Common Pitfalls
-
Yielding from main thread causes errors as main is not a coroutine
-
Not checking resume success misses errors thrown inside coroutines
-
Creating new coroutines in loops without cleanup causes memory leaks
-
Yielding across C call boundaries fails in standard Lua (works in LuaJIT)
-
Assuming coroutines are threads leads to race condition concerns that
don't exist
-
Not handling coroutine completion causes errors when resuming dead
coroutines
-
Overusing coroutines for simple iteration adds complexity without benefits
-
Mixing coroutine.create and wrap interfaces causes confusion
-
Forgetting to resume coroutines in schedulers leaves tasks suspended
forever
-
Passing wrong number of arguments to resume causes unexpected behavior
When to Use This Skill
Apply coroutines for cooperative multitasking where explicit control flow is
beneficial.
Use generators and iterators when implementing lazy evaluation or infinite
sequences.
Leverage coroutines for async I/O patterns to avoid callback complexity and
maintain sequential code style.
Implement state machines with coroutines for game AI, parsers, or protocol
handlers.
Use producer-consumer patterns when processing data through transformation
pipelines.
Apply coroutine-based schedulers for managing multiple concurrent operations
cooperatively.
Resources