Skip to content

Scheduler

Each environment owns a fake scheduler. The task global is bound to that scheduler, which makes scheduled work deterministic in tests.

local env = getEnvironment()

local done = false

task.delay(1, function()
    done = true
end)

env.scheduler:advance(0.5)
assert(done == false)

env.scheduler:advance(0.5)
assert(done == true)

task API

The sandboxed task global supports:

  • task.spawn(callback, ...)
  • task.defer(callback, ...)
  • task.delay(seconds, callback, ...)
  • task.wait(seconds)
  • task.cancel(handle)

spawn creates a managed coroutine and queues it immediately. defer queues a callback for the current scheduler time. delay queues a callback for a later scheduler time.

local order = {}

task.defer(function()
    table.insert(order, "defer")
end)

task.spawn(function()
    table.insert(order, "spawn")
end)

getEnvironment().scheduler:flush()

assert(#order == 2)

task.cancel(handle) marks a queued handle as cancelled. Cancelled items are skipped when the scheduler flushes.

Waiting

task.wait(seconds) can only yield inside a scheduler-managed thread. Use task.spawn when testing yielding code directly.

local env = getEnvironment()
local finished = false

task.spawn(function()
    task.wait(2)
    finished = true
end)

env.scheduler:flush() --starts the thread

env.scheduler:advance(1)
assert(finished == false)

env.scheduler:advance(1)
assert(finished == true)

Top-level script execution cannot yield through the fake scheduler. A top-level task.wait raises an error instead of suspending the runner.

Advancing Time

The scheduler object supports:

  • scheduler:flush(): runs queued work whose scheduled time is less than or equal to the current time.
  • scheduler:advance(delta): moves fake time forward and flushes ready work.
  • scheduler:runAll(): runs all queued work, advancing time to each pending item as needed.
  • scheduler:now(): returns current fake time.
  • scheduler:inspect(): returns queued item summaries.
  • scheduler:canYieldCurrentThread(): returns whether the current coroutine is scheduler-managed.

When time advances by a positive amount, the environment fires RunService.Heartbeat, RunService.Stepped, and RunService.RenderStepped.

WaitForChild

Instance:WaitForChild(name, timeout) uses the scheduler when called from a managed thread. It returns immediately if the child exists, returns the child when a matching ChildAdded signal fires, or returns nil on timeout.

local env = getEnvironment()
local folder = Instance.new("Folder", workspace)
local found

task.spawn(function()
    found = folder:WaitForChild("Child", 1)
end)

task.defer(function()
    local child = Instance.new("Folder", folder)
    child.Name = "Child"
end)

env.scheduler:runAll()
assert(found == folder.Child)

Outside a managed thread, WaitForChild returns nil when the child is missing.