Coroutine Basics: Create, Resume, Yield
Coroutines are a powerful feature in Lua, enabling you to build cooperative multitasking without the complexity of threads. A coroutine is essentially a function that can pause mid-execution and resume later from exactly where it left off. This makes them perfect for iterators, producers and consumers, and asynchronous-style code patterns.
Unlike operating system threads, coroutines are entirely managed by Lua. You explicitly yield control when you want to pause, and resume when you want to continue. This makes coroutine code predictable and safe, since there’s no race conditions or preemptive scheduling to worry about.
Creating a Coroutine
You create a coroutine with coroutine.create, passing in the function you want to run:
local co = coroutine.create(function()
print("Hello from my coroutine")
end)
This doesn’t actually run the function yet. The coroutine starts in a suspended state. You can check this with coroutine.status(co), which returns the string “suspended” for newly created coroutines.
The coroutine handle returned by create is a thread value. You use this handle to control the coroutine throughout its lifecycle.
Starting and Resuming
You start or continue a coroutine with coroutine.resume. On the first call, any arguments you pass become parameters to the coroutine’s main function:
local co = coroutine.create(function(name)
print("Hello, " .. name)
end)
coroutine.resume(co, "Alice") -- prints: Hello, Alice
This is a common point of confusion: arguments to the first resume go to the function parameters, not to any yield. We’ll cover how to pass data the other direction next.
When you call resume, the coroutine runs until it either finishes or hits a yield. On success, resume returns true followed by any values passed to yield. If something goes wrong, it returns false plus an error message.
Pausing with Yield
Inside a coroutine, you call coroutine.yield to pause execution and transfer control back to the caller:
local co = coroutine.create(function()
print("First line")
coroutine.yield()
print("Third line")
end)
coroutine.resume(co) -- prints: First line
coroutine.resume(co) -- prints: Third line
The coroutine pauses at yield and returns control to whoever called resume. The next time you resume the same coroutine, execution continues right after that yield.
You can pass values to yield, which become the return values of the corresponding resume call:
local co = coroutine.create(function()
local result = coroutine.yield("paused")
print("Resumed with: " .. result)
end)
local ok, value = coroutine.resume(co)
print(value) -- prints: paused
coroutine.resume(co, "hello world") -- prints: Resumed with: hello world
The data flows both directions: you can pass arguments to resume that become the return values of yield, and values you pass to yield become the return values of resume.
Coroutine States
A coroutine can be in one of four states at any time:
- suspended: Created but not running, or paused at a yield
- running: Currently executing (check with
coroutine.statusfrom inside, or get the current coroutine withcoroutine.running()) - normal: Active but currently resuming another coroutine
- dead: Finished execution or encountered an error
After a coroutine runs to completion (or returns from its function), it becomes dead. You cannot resume a dead coroutine—attempting to do so returns an error. Always check the status before resuming if you’re not sure:
if coroutine.status(co) == "suspended" then
coroutine.resume(co)
end
Using coroutine.wrap for Simpler APIs
The coroutine.wrap function provides a simpler interface for common patterns. Instead of dealing with create and resume directly, it returns a regular function that resumes the coroutine each time you call it:
local producer = coroutine.wrap(function()
for i = 1, 5 do
coroutine.yield(i)
end
end)
print(producer()) -- 1
print(producer()) -- 2
print(producer()) -- 3
This is particularly useful for iterators—you’ll see this pattern throughout Lua code. The trade-off is that wrap doesn’t return a success boolean like resume does. Instead, errors are raised as exceptions, so you’ll need pcall if you want to handle errors gracefully:
local fn = coroutine.wrap(function()
error("something went wrong")
end)
local ok, err = pcall(fn) -- ok = false, err = error message
Also note that you cannot use coroutine.status on wrapped coroutines since you don’t have direct access to the thread handle.
Common Use Cases
Iterators
Coroutines shine for building custom iterators. Instead of building state machines manually, you can write a generator function that yields each value:
function fibonacci(limit)
return coroutine.wrap(function()
local a, b = 0, 1
while a <= limit do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
for n in fibonacci(100) do
print(n)
end
-- Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
This is much cleaner than implementing an iterator object with __iter and __next metamethods. The coroutine handles all the state for you.
Producer-Consumer Patterns
You can use coroutines to decouple producers from consumers. The producer generates data and yields it, the consumer processes it and optionally sends more work back:
local producer = coroutine.create(function()
for i = 1, 10 do
local shouldContinue = coroutine.yield(i * 10)
if not shouldContinue then
break
end
end
end)
while true do
local ok, value = coroutine.resume(producer)
if not ok then
print("Producer error: " .. value)
break
elseif coroutine.status(producer) == "dead" then
break
end
print("Received: " .. value)
-- Optionally send signal to continue
if value >= 80 then
break
end
end
This pattern appears in networking code, game engines, and anywhere you need to handle items asynchronously.
Cooperative Task Scheduling
You can build a simple task scheduler that runs multiple tasks in sequence, giving each a slice of time:
local tasks = {}
function addTask(name, fn)
table.insert(tasks, coroutine.create(fn))
end
addTask("Task A", function()
for i = 1, 3 do
print("A " .. i)
coroutine.yield()
end
end)
addTask("Task B", function()
for i = 1, 3 do
print("B " .. i)
coroutine.yield()
end
end)
while #tasks > 0 do
local deadTasks = {}
for i, co in ipairs(tasks) do
if coroutine.status(co) == "suspended" then
coroutine.resume(co)
elseif coroutine.status(co) == "dead" then
table.insert(deadTasks, i)
end
end
-- Remove dead tasks
for i = #deadTasks, 1, -1 do
table.remove(tasks, deadTasks[i])
end
end
print("All tasks done")
This gives you a basic cooperative multitasking system within a single thread.
Pitfalls to Avoid
Passing Arguments to the Wrong Place
A common mistake is expecting the first resume arguments to go to yield. They don’t—they become parameters to the function:
-- WRONG: "hello" goes to the function parameter, not to yield
local co = coroutine.create(function()
print(coroutine.yield())
end)
coroutine.resume(co, "hello") -- prints: (nothing)
-- CORRECT: pass initial args to the function
local co = coroutine.create(function(msg)
print(msg)
coroutine.yield("done")
end)
coroutine.resume(co, "hello") -- prints: hello
Resuming a Dead Coroutine
Once a coroutine finishes, you can’t resume it again:
local co = coroutine.create(function()
return "finished"
end)
coroutine.resume(co)
local ok, err = coroutine.resume(co) -- err: cannot resume dead coroutine
Check coroutine.status(co) before resuming if you’re not sure.
Yielding Through Protected Calls
In Lua 5.1, you cannot yield through pcall or C function calls. If you try, you’ll get an error:
-- This fails in Lua 5.1
local co = coroutine.create(function()
pcall(function()
coroutine.yield()
end)
end)
coroutine.resume(co) -- error
Lua 5.2 and later allow yielding through pcall, but avoid doing this inside metamethods or C library calls.
Expecting wrap to Work Like resume
Remember that coroutine.wrap doesn’t return success booleans. If you need error handling, wrap the call in pcall:
local fn = coroutine.wrap(function()
error("oops")
end)
-- This won't work as expected:
local ok, err = fn() -- err is raised, not returned
-- Use pcall instead:
local ok, err = pcall(fn) -- ok = false, err = "oops"
Checking If You Can Yield
Before calling yield, you can use coroutine.isyieldable() to check if it’s safe to yield. This is useful inside helper functions that might be called from both coroutine and non-coroutine contexts:
function maybeYield()
if coroutine.isyieldable() then
coroutine.yield()
end
end
If you call yield when it’s not safe (e.g., from outside a coroutine or inside a C function), Lua will raise an error.
See Also
- Lua Reference Manual: Coroutines — official documentation