Skip to content

Coroutines

Lesson 8.2

A coroutine is a function that can pause in the middle and continue later from the same spot. It is the tool for things that happen step by step over time.

Pause and continue

A normal function runs from start to finish in one go. A coroutine can stop with coroutine.yield(), hand control back, and later pick up exactly where it stopped, with all its local variables intact.

local co = coroutine.create(function()
  print("step 1")
  coroutine.yield()
  print("step 2")
  coroutine.yield()
  print("step 3")
end)

coroutine.resume(co)  --> step 1
coroutine.resume(co)  --> step 2
coroutine.resume(co)  --> step 3
  • coroutine.create(f) makes a coroutine from a function. It doesn’t run yet.
  • coroutine.resume(co) runs it until the next yield, or until the function ends.
  • coroutine.yield() pauses it.

Coroutines are not threads. Only one piece of code runs at a time, and a coroutine only pauses where it calls yield. That makes them safe and simple: nothing can change your variables behind your back.

Status

coroutine.status(co) tells you the state:

Status Means
"suspended" created but not started, or paused at a yield
"running" it is the code running right now
"normal" it resumed another coroutine and waits for it
"dead" it finished, or it stopped with an error
local co = coroutine.create(function() coroutine.yield() end)
print(coroutine.status(co))  --> suspended
coroutine.resume(co)
print(coroutine.status(co))  --> suspended
coroutine.resume(co)
print(coroutine.status(co))  --> dead
print(coroutine.resume(co))  --> false	cannot resume dead coroutine

Passing values in and out

yield and resume pass values both ways. Values given to yield come out of resume, after a true. Values given to the next resume come out of yield:

local co = coroutine.create(function(a)
  print("started with", a)
  local b = coroutine.yield(a * 2)
  print("got", b)
  return "done"
end)

print(coroutine.resume(co, 5))
print(coroutine.resume(co, "hello"))
started with	5
true	10
got	hello
true	done

The first resume passes its arguments to the function’s parameters. Each later resume passes its arguments as the result of the yield that is waiting.

Errors in coroutines

An error inside a coroutine does not crash the caller. resume returns false and the message, and the coroutine is dead:

local co = coroutine.create(function()
  error("broken", 0)
end)
print(coroutine.resume(co))  --> false	broken

coroutine.wrap

coroutine.wrap(f) makes a coroutine and gives you a plain function that resumes it. It returns only the yielded values, without the true, and it raises errors instead of returning false. It is the easy way to write generators, functions that produce one value per call:

local function countdown(n)
  return coroutine.wrap(function()
    for i = n, 1, -1 do
      coroutine.yield(i)
    end
  end)
end

local next_number = countdown(3)
print(next_number())  --> 3
print(next_number())  --> 2
print(next_number())  --> 1

A wrapped coroutine also works as an iterator in a generic for, because the loop stops when the function returns nil:

local function chunks_around(cx, cz, r)
  return coroutine.wrap(function()
    for x = cx - r, cx + r do
      for z = cz - r, cz + r do
        coroutine.yield(x, z)
      end
    end
  end)
end

local count = 0
for x, z in chunks_around(0, 0, 1) do
  count = count + 1
end
print(count)  --> 9

Writing this with a plain closure would mean tracking x and z by hand between calls. With a coroutine the nested loops just pause.

Spreading work over time

Games use coroutines for things that take several ticks: a cutscene, an animation, a countdown, or a big job split into small pieces so the game never stalls. The pattern: write the whole sequence as one function that yields where it should wait, and resume it once per tick.

local function build_tower(height)
  return coroutine.create(function()
    for y = 1, height do
      print("placing block at y=" .. y)
      coroutine.yield()           -- wait for the next tick
    end
    print("tower done")
  end)
end

local job = build_tower(3)

-- pretend this is the game calling us every tick
for tick = 1, 10 do
  if coroutine.status(job) == "dead" then break end
  coroutine.resume(job)
end
placing block at y=1
placing block at y=2
placing block at y=3
tower done

Celestial has helpers for this: cl.spawn(fn) starts a function as a coroutine, and inside it cl.sleep_ticks(20) waits one second without you resuming anything by hand. Part 2 shows them. They are coroutines underneath.

Other functions

  • coroutine.running() returns the running coroutine, and true if it is the main program.
  • coroutine.isyieldable() tells you whether the current code may call yield.
  • coroutine.close(co) closes a suspended coroutine, runs its pending <close> variables and marks it dead.

Try it

Write a generator ids(prefix) with coroutine.wrap that yields prefix .. 1, prefix .. 2 and so on, forever. Take the first three IDs from ids("mob_"). Then write a coroutine traffic_light that yields "green", "yellow", "red" in a loop, and resume it five times, printing each colour.

Show a solution
local function ids(prefix)
  return coroutine.wrap(function()
    local n = 0
    while true do
      n = n + 1
      coroutine.yield(prefix .. n)
    end
  end)
end

local next_id = ids("mob_")
print(next_id(), next_id(), next_id())  --> mob_1	mob_2	mob_3

local traffic_light = coroutine.create(function()
  while true do
    coroutine.yield("green")
    coroutine.yield("yellow")
    coroutine.yield("red")
  end
end)

for i = 1, 5 do
  local _, colour = coroutine.resume(traffic_light)
  print(colour)
end

The loop prints green, yellow, red, green, yellow. An endless loop is fine inside a coroutine, because it pauses at every yield.

Recap: create makes a coroutine, resume runs it until yield, values flow both ways, and wrap turns it into a generator.