Results and Futures
UpdatedFuture<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.
