Coroutine Basics: Create, Resume, Yield

· 7 min read · Updated March 18, 2026 · intermediate
coroutines multitasking

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.status from inside, or get the current coroutine with coroutine.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