Coroutine Basics: Create, Resume, Yield
Understanding coroutine basics opens up cooperative multitasking in Lua 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.
Prerequisites
Before working through this tutorial, you should have Lua 5.1 or later installed and be comfortable writing basic functions, passing arguments, and working with return values. If you are new to Lua’s function syntax, review the language reference or a beginner guide first. Having the Lua coroutine manual page open in another tab is helpful for looking up edge cases as you go.
Introduction to coroutine basics
Lua coroutines are collaborative, non-preemptive threads of execution. Unlike full operating system threads, they do not run in parallel; only one coroutine executes at a time. The programmer decides when each coroutine yields, which eliminates the need for locks, mutexes, and other concurrency primitives. Coroutines are a built-in language feature in Lua, not a library bolted on top, which means they integrate cleanly with the rest of the runtime: garbage collection, error handling, and the C API all work naturally with coroutines.
Coroutines appear in many Lua applications: game engines use them for AI behaviour trees, web frameworks use them for non-blocking I/O, and data pipelines use them for lazy iteration over large datasets. By the end of this tutorial you will be able to create, resume, yield, and debug coroutines, and you will recognise the common patterns that appear across the Lua ecosystem.
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. Calling create allocates a new Lua thread internally but does not begin execution; the function body sits waiting for the first resume.
The coroutine handle returned by create is a thread value, distinct from operating system threads. You use this handle to control the coroutine throughout its lifecycle: resuming it, checking its status, and eventually letting it run to completion. Once created, a coroutine remains in memory until it either finishes naturally or is garbage collected after becoming unreachable.
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 (an uncaught runtime error inside the coroutine), it returns false plus an error message string. This two-return-value convention lets you distinguish between a normal yield and a fatal error without wrapping everything in pcall.
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. This suspend-and-resume cycle is the heart of cooperative multitasking: each coroutine voluntarily gives up the CPU, and no external scheduler forces a context switch.
A key detail that many tutorials gloss over is that yield can carry data in both directions. 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
The normal state deserves extra attention because it is the least intuitive. When coroutine A calls resume on coroutine B, coroutine A enters the normal state while B runs. This matters because a coroutine in the normal state cannot be resumed or yielded from directly: Lua maintains a single call stack per thread, and normal means the thread is waiting for a nested resume to return.
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. This is the preferred approach when you plan to use the coroutine purely as a generator or iterator: you call one function repeatedly and get back one value at a time, with no need to manage thread handles or check status codes manually:
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. The producer never blocks waiting for the consumer to finish: it simply yields the next value and the consumer picks it up on the next resume cycle.
Cooperative task scheduling
When you have more than two coroutines, you need a scheduler to decide who runs next. A round-robin scheduler iterates through a list of coroutines, resuming each one that is still suspended and removing any that have died. This gives each task a fair share of execution time without any preemption:
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. The scheduler is entirely deterministic: tasks run in insertion order, and each one yields voluntarily. In production code you would add priority queues or time-slice limits, but the core mechanic remains the same.
Pitfalls to Avoid
Coroutines are simple in concept, but a handful of edge cases catch even experienced Lua programmers off guard. The next few sections cover the most frequent mistakes and how to avoid them.
Passing arguments to the wrong place
A common mistake is expecting the first resume arguments to go to yield. They do not: those initial arguments become parameters to the coroutine’s main function instead. This trips up newcomers because the data flow feels backwards at first, but the rule is simple once you remember it:
-- 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 running its function body or hits an uncaught error, it transitions to the dead state permanently. There is no way to reset or restart a dead coroutine; you must create a new one if you need to run the same logic again. Attempting to resume a dead coroutine produces a runtime error rather than silently failing:
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. This is especially important in long-running programs where a coroutine may have finished many cycles ago and you have lost track of its state.
Yielding through protected calls
In Lua 5.1, you cannot yield through pcall or C function calls. The runtime treats a yield attempt inside a protected call as an attempt to cross a C boundary, which is disallowed. 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. The safest approach is to keep yields at the top level of your coroutine function and use helper functions that return values rather than yielding internally.
Expecting wrap to work like resume
coroutine.wrap discards the success boolean that resume provides, which means errors propagate as exceptions instead of being returned as a second value. 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"
Also note that you cannot use coroutine.status on wrapped coroutines since you don’t have direct access to the thread handle. The wrapper function hides the underlying coroutine entirely, which is convenient for simple iterators but limiting when you need to inspect or debug coroutine state.
Checking if you can yield
Before calling yield, you can use coroutine.isyieldable() to check if it’s safe to yield. This function was introduced in Lua 5.3 and returns true when the calling thread is a coroutine that is allowed to yield. It is especially 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. These coroutine basics give you a solid foundation. The remaining tutorials in this series build on everything covered here.
Next steps
Now that you understand the fundamentals of creating, resuming, and yielding coroutines, you are ready to put them to work in real patterns. The next tutorial in this series, Cooperative multitasking, shows how to build a full task scheduler that runs multiple coroutines in round-robin fashion. After that, Producer-consumer covers the classic decoupling pattern where one coroutine generates values and another processes them.
See Also
- Cooperative multitasking: build a task scheduler with Lua coroutines
- Producer-consumer pattern: decouple data generation from processing
- Lua Reference Manual: Coroutines: official documentation