lloyAlloy on GitHub
Results and Futures
Docs

Results and Futures

Updated

Future<T> and Result<T, E> answer different questions. A Future answers when: the value is not ready. A Result answers whether: the work can fail. Most Roblox work needs both answers.

              cannot fail    can fail
no yield      T              Result<T, E>
yields        Future<T>      async function f(): Result<T, E>

Use a Result alone when the failure is an expected outcome: a lookup that misses, input that does not validate, a trade the rules refuse. Nothing throws in these cases.

struct Loadout
    slot: number
    tool: string
end

local function equip(slot: number, tool: string): Result<Loadout, string>
    if slot < 1 or slot > 9 then
        return Err(`slot {slot} is outside 1 to 9`)
    end
    return Ok(new Loadout { slot = slot, tool = tool })
end

Use a Future alone when the work yields and cannot fail: task.wait, Future.delay, your own animation steps.

Use both when the work yields and can fail. This is most real Roblox work: DataStore, HttpService, MarketplaceService. Declare the async function to answer with a Result, then try await reads the value.

async function load_profile(id: number): Result<Profile, string>
    local raw = try await store:get(id)
    return Ok(parse(raw))
end

Return an Err. Do not throw and then catch your own throw. A throw loses the error type and costs a traceback.

Result.pcall is for a foreign call that throws. Roblox APIs throw, so wrap one at the boundary and let Results flow inward. Do not spread Result.pcall through game logic.

Throw with error or assert when the failure means the code is wrong. Ask if a correct caller with valid data reaches it. If no, throw. A Result that every caller unwraps is a throw with more steps.

A Future in a signature makes every caller async, so do not answer with one for work that never yields. alloy doc await says where an await may stand.