Skip to content

Async Await

Kevin edited this page Jun 16, 2026 · 3 revisions

astal-lua provides the async and await decorators to simplify asynchronous I/O operations, allowing functions to run in async-enabled contexts. Internally, they rely on Gio.Async, which transforms Gio-style asynchronous functions into synchronous-like code.

In lgi, many async operations follow a callback-based pattern:

  • name_async starts the operation and registers a callback.
  • name_finish is called inside the callback to retrieve the result.

While common in Gio, this can lead to deeply nested callback chains (callback hell). Gio.Async allows you to wrap these functions using Lua coroutines, so you can write linear, non-blocking code.

Async

The async decorator wraps a function to accept a callback. The function inside does not return a value, so the result must be handled via the callback, similar to Gio’s _async functions.

---@generic F: function
---@param fn F
---@return F
local async = function(fn)
    return function(...) return Gio.Async.start(fn)(...) end
end

Callback-style async function

---@async
local async_read_file = function(filename)
    local file = Gio.File.new_for_path(filename)
    local info = file:async_query_info('standard::size', 'NONE')
    local stream = file:async_read()
    local bytes = stream:async_read_bytes(info:get_size())
    assert(stream:async_close())
    return bytes.data
end

local read_file_async = async(function(filename, callback)
    callback(async_read_file(filename))
end)

read_file_async('/etc/passwd', print) -- works
local function ExampleButton()
    return Widget.Button {
        label = 'Dump!',
        on_clicked = async(function()
            local contents = async_read_file('/etc/passwd')
            print(contents)
        end),
    }
end

Warning

Calling an async_name function outside an async-enabled context will not work.

Await

The await decorator allows calling async functions and returning their result directly, without using callbacks. The parent function must be executed in an async-enabled context (---@async).

---@generic F: function
---@param fn F
---@return F
local await = function(fn)
    return function(...) return Gio.Async.call(fn)(...) end
end

Unlike async, which schedules a function in the background, await blocks only the coroutine, not the main thread, keeping the UI responsive.

local read_file = await(function(path)
    return async_read_file(path)
end)

local function OtherExampleButton()
    return Widget.Button {
        label = 'DumpAgain!',
        on_clicked = function()
            local contents = read_file('/etc/passwd') -- This wont block the UI
            print(contents)
        end,
    }
end

Warning

Do not call an await function inside another async function via a wrapper. For example:

local async_fn = function() end
local fn = await(function() return async_fn() end)

local example = async(function()
    local result = fn() -- ❌ Don't do this
end)

Instead, call the original async function directly:

local example = async(function()
    local result = async_fn() -- ✅ Correct
end)

Tip

Some libraries expose asynchronous methods through corresponding _finish functions. Even if a method does not have an _async suffix, you can inspect it to determine whether it can be used from async-enabled functions.

A quick way to check is:

print(MyClass.my_function)

If a matching *_finish method exists internally, the function may support asynchronous invocation patterns.

local Pam = lgi.require("AstalAuth").Pam

print(Pam.authenticate)       -- valid method
print(Pam.async_authenticate) -- valid async method

-- Pam.authenticate_finish also exists internally
-- which indicates async support through the Gio.Async pattern.

This is particularly useful when working with GObject-based libraries, where asynchronous APIs are often implemented as method() + method_finish() pairs, even when the public API naming does not strictly follow a *_async convention.

Clone this wiki locally