Skip to content

Async API

async has two different jobs:

  • Wait for I/O without blocking the server or a worker: async.fetch() and async.sleep().
  • Run Lua code on a selected executor: async.task() and async.run().

Load it with:

local async = require "async"
GoalUse
Wait for an HTTP responseasync.fetch(url)
Wait for a number of ticksasync.sleep(ticks)
Run expensive Lua code in parallelasync.task("threadpool", fn)
Run Lua code on the Minecraft server threadasync.task("main", fn)
Run work and wait for its resultasync.run(executor, fn)
Coordinate manually-settled workasync.promise()
Protect a shared Lua sectionasync.mutex()

Do not put async.fetch() in a thread-pool task just to make the request asynchronous. HTTP requests are already non-blocking:

local response = async.fetch("https://example.com/data")

Use the thread pool for CPU-heavy Lua work.

Every task requires an executor:

ExecutorIntended use
"main"Minecraft APIs, entities, worlds, players, inventories, and other server state
"threadpool"Expensive pure-Lua calculations and independent background work

"main" runs Lua on the Minecraft server thread. Keep it short. A long calculation there pauses the server tick.

"threadpool" runs Lua away from the server thread. Do not access Minecraft objects from it:

-- Safe: pure computation
local task = async.task("threadpool", function()
return generate_mesh(input)
end)
-- Unsafe: Minecraft state belongs on the main executor
async.task("threadpool", function()
player:sendMessage("hello")
end)

Runs a function on the selected executor and waits for its result. All return values are preserved, and errors are thrown in the calling coroutine.

local total = async.run("threadpool", function(a, b)
return expensive_calculation(a, b)
end, 10, 20)

Use "main" when the function must interact with Minecraft:

async.run("main", function()
player:sendMessage("The calculation is complete")
end)

async.run() must be called from a coroutine because it may yield while waiting.

Starts a function and immediately returns a task object:

local task = async.task("threadpool", function()
return expensive_calculation()
end)
local result = task:wait()

The function receives the arguments after fn:

local task = async.task("threadpool", function(x)
return x * 2
end, 21)

Waits for completion and returns the raw result values. If the task fails, it throws the task error.

local value = async.task("threadpool", function()
return 42
end):wait()

Waits for completion and returns true, result... on success or false, error on failure.

local ok, value = async.task("threadpool", function()
return risky_calculation()
end):try()
if not ok then
print("Calculation failed: " .. value)
end

true after the task has resolved or rejected.

One of:

  • "pending"
  • "resolved"
  • "rejected"

Returns "task" for task objects and "promise" for promise objects.

Tasks can call other coroutine-aware operations while running:

local task = async.task("threadpool", function()
async.sleep(20)
local response = async.fetch("https://example.com/data")
return response.json
end)

Start independent calculations first, then wait for all of them:

local left = async.task("threadpool", function()
return generate_chunk(1)
end)
local right = async.task("threadpool", function()
return generate_chunk(2)
end)
local results = async.all(left, right)
local left_chunk = results[1].value
local right_chunk = results[2].value

The calculations can run concurrently. Apply the resulting data through the main executor if it touches Minecraft:

async.run("main", function()
apply_chunk(left_chunk)
apply_chunk(right_chunk)
end)

Waits for every task or promise. It accepts either varargs or an array-like table:

local results = async.all(task1, task2)
-- or:
local results = async.all { task1, task2 }

It throws the first rejection after all inputs have settled. On success, it returns an array of result entries:

{
{ ok = true, value = first_value },
{ ok = true, value = second_value }
}

The result entry currently stores the first returned value. Use individual task:wait() calls when you need multiple return values from each task.

Waits for every input and never throws because of a rejected task:

local results = async.allSettled(task1, task2)
for i, result in ipairs(results) do
if result.ok then
print(i, result.value)
else
print(i, "failed: " .. result.error)
end
end

Each entry has either:

{ ok = true, value = value }

or:

{ ok = false, error = "error message" }

async.promise() creates an awaitable that you settle manually:

local promise = async.promise()
mc.schedule(20, function()
promise:resolve("finished")
end)
print(promise:wait())

Resolves the promise and returns true if this was the first settlement.

Rejects the promise and returns true if this was the first settlement.

The first settlement wins:

local promise = async.promise()
print(promise:resolve(1)) -- true
print(promise:resolve(2)) -- false

Promises also expose done, state, wait(), and try().

Suspends the current coroutine for a number of server ticks. Twenty ticks is approximately one second.

async.sleep(40)
print("Two seconds passed")

It does not block the executor thread. The coroutine resumes automatically; do not call coroutine.resume() yourself.

Sends a GET request and suspends the current coroutine until the response arrives:

local response = async.fetch("https://api.example.com/data")
if response.ok then
print(response.text)
else
print("HTTP request failed: " .. (response.error or "unknown error"))
end
local response = async.fetch {
url = "https://api.example.com/data",
method = "POST",
headers = {
Authorization = "Bearer token"
},
json = { key = "value" },
timeout = 10
}
OptionTypeDefaultDescription
urlstringrequiredRequest URL
methodstring"GET"HTTP method
headerstable{}Request headers
bodystringnilRaw request body
jsontablenilJSON request body; sets Content-Type when absent
timeoutnumber10Timeout in seconds

body and json are mutually exclusive.

FieldTypeDescription
okbooleantrue for HTTP status codes in the 2xx range
statusnumber or nilHTTP status code when a response was received
textstring or nilResponse body
headerstable or nilResponse headers
jsontable or nilLazily parsed JSON body
errorstring or nilNetwork or request error

HTTP errors such as 404 are returned as responses with ok = false. Transport failures also return ok = false, but may not have a status code.

async.mutex() creates a coroutine-friendly mutex:

--# nova syntax
local mutex = async.mutex()
local value = mutex:with \{
return update_shared_cache()
}

Only one callback owns the mutex at a time. Waiting callbacks yield instead of blocking a Java thread. The callback may use async.sleep(), async.fetch(), or task:wait().

The mutex is released when the callback returns or throws:

--# nova syntax
local ok, err = pcall \{
mutex:with \{
error("the lock is still released")
}
}

Mutexes are non-reentrant. Avoid waiting for another mutex while holding one, because that can deadlock.

async.sleep(), async.fetch(), task:wait(), task:try(), async.run(), async.all(), async.allSettled(), and mutex:with() may yield. They require a coroutine-backed execution context.

Commands, scheduled callbacks, tasks, and coroutine-backed event handlers can use these APIs. The runtime resumes suspended coroutines automatically.

Do not manually resume a coroutine suspended by an async operation.

Keep Minecraft work on the main executor and calculation work on the thread pool:

local generated = async.run("threadpool", function()
return generate_data()
end)
async.run("main", function()
place_generated_data(generated)
end)

Do not pass live Minecraft objects into thread-pool code and access them there. Extract plain data on the main executor first, then pass the data to the calculation.