Implementing Cooperative Multitasking with Lua Coroutines: A Practical Guide
Learn how to use Lua coroutines for cooperative multitasking: define yielding workers, create and drive coroutines, check status, and handle errors and cleanup.
07 May 2026, 14:47 UTC

Desired outcome
Run several logical tasks inside a single Lua state without creating OS threads, letting each task pause and resume cooperatively.
Prerequisites
- Lua version 5.0 or newer (coroutines were introduced in 5.0). Verify with
lua -vor by checking_VERSIONat runtime. - A Lua interpreter or an embedded Lua state that you can execute scripts against.
- Basic familiarity with Lua functions and the ability to edit and save a
.luafile. - Access to a terminal or command prompt where you can run the interpreter (no special privileges required; normal user rights are sufficient).
Focused procedure
- Define a worker function that performs a small unit of work and yields control with
coroutine.yield(). The function should accept any arguments you need and return values when it finishes. - Create a coroutine from the worker using
coroutine.create(worker). This returns a coroutine object but does not start it. - Start and drive the coroutine with a loop that calls
coroutine.resume(co, ...). Pass the same arguments each time (ornilif none are needed). - Check the result of each resume: the first return value indicates success (
true) or failure (falsefollowed by an error message). - Terminate when the coroutine is dead: after a successful resume, check
coroutine.status(co); when it returns'dead'the worker has finished.
Example script
-- worker.lua
local function worker(name, limit)
for i = 1, limit do
print(name .. ': step ' .. i)
-- yield after each step so other coroutines can run
coroutine.yield(i)
end
return name .. ' finished'
end
local co1 = coroutine.create(function() return worker('A', 3) end)
local co2 = coroutine.create(function() return worker('B', 2) end)
local function drive(co)
local ok, val = coroutine.resume(co)
if not ok then
error('coroutine error: ' .. tostring(val))
end
return ok, val
end
while coroutine.status(co1) ~= 'dead' or coroutine.status(co2) ~= 'dead' do
if coroutine.status(co1) ~= 'dead' then
local ok, val = drive(co1)
if ok then print('resumed A ->', val) end
end
if coroutine.status(co2) ~= 'dead' then
local ok, val = drive(co2)
if ok then print('resumed B ->', val) end
end
end
print('All coroutines finished')
Save the above as worker.lua and run it with:
lua worker.lua
The script creates two coroutines that each print a few steps, yielding after each step so the other can run. The driver loop resumes each coroutine until both report status 'dead'.
Expected checks
- After each
coroutine.resumecall, verify the first return value istrue. If it isfalse, the second value is an error message; handle it withpcallor by logging and deciding whether to abort. - Observe
coroutine.status(co)transitioning from'suspended'(immediately after creation) to'running'during a resume, and finally to'dead'when the worker function returns or throws an error. - Optionally add debug prints inside the worker before and after
coroutine.yieldto confirm that yields occurred as expected.
Recovery options and limitations
- Error handling: Wrap each
coroutine.resumeinpcallto catch errors without breaking the driver loop. If an error occurs, you can log it, create a fresh coroutine from the same worker, or propagate the error to a supervisor. - Resource cleanup: Any resources (e.g., file handles, timers) allocated inside the worker should be released in a
pcall-protected cleanup block before the coroutine is abandoned, because the coroutine object may linger until its references are dropped. - Yield restrictions: Do not call
coroutine.yieldfrom a C function or from a Lua function invoked via the C API unless that function is marked yieldable. Yielding across a non‑yieldable C boundary raises an error. - Stack usage: Deep recursion or excessive yielding can consume Lua stack space. Monitor stack depth (e.g., via
debug.getinfo) and consider increasing the stack size withlua_setstacksizeif you embed Lua, or redesign the algorithm to avoid unbounded recursion. - Garbage collection: A coroutine remains collectable only when there are no references to it. After a coroutine reaches
'dead', ensure you drop any variables pointing to it (e.g., set them tonil) so Lua’s GC can reclaim its memory.
Practical verification
To confirm that the coroutine mechanism works as described, run the example script and observe the interleaved output. The printed steps should alternate between the two workers, reflecting the yields. If you see only one worker’s output before the other begins, check that each worker actually calls coroutine.yield and that the driver loop resumes both coroutines each iteration.
Additionally, test error handling by modifying the worker to call error('test') after a few yields; the driver should catch the false return from coroutine.resume and print the error message without terminating the whole program.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.