Async and Events
Promise
Values that arrive later. Every Engine method that ends in Async returns a Promise.
Getting it
local Promise = Engine:GetPackage("Promise")Overview
A Promise represents work that finishes later: a DataStore read, a purchase prompt, a remote request. Instead of yielding your thread, the work runs on its own and you attach what should happen next. Engine's API follows the widely used Roblox Promise design, so code written for that library works the same here. The one naming difference: the current state is the Status property instead of a getStatus() method.
A Promise is always in one of four states:
| Status | Meaning |
|---|---|
"Started" | The work is still running. |
"Resolved" | The work finished and produced values. |
"Rejected" | The work failed or errored. |
"Cancelled" | Someone cancelled the Promise before it settled. |
local Promise = Engine:GetPackage("Promise")
local function loadCoins(player: Player)
return Promise.new(function(resolve, reject)
local ok, coins = pcall(function()
return store:GetAsync(tostring(player.UserId))
end)
if ok then
resolve(coins or 0)
else
reject(coins)
end
end)
end
loadCoins(player)
:andThen(function(coins)
print(player.Name, "has", coins, "coins")
end)
:catch(function(err)
warn("could not load coins:", tostring(err))
end)Errors thrown inside an executor or a handler reject the Promise instead of crashing your script. A rejection that nobody handles prints Unhandled Promise rejection with the place it was created, one frame after it happens.
Waiting for a result
When you are already in a thread that may yield, :await() and :expect() read the result directly:
local ok, coins = loadCoins(player):await() -- ok is false when it rejected
local coins = loadCoins(player):expect() -- errors when it rejectedCreating promises
new
Promise.new(executor: (resolve, reject, onCancel) -> ()): PromiseRuns executor right away in a new thread. Call resolve(...) or reject(...) once; later calls are ignored. onCancel(fn) registers a function that runs if the Promise is cancelled while still started, and returns true if it is already cancelled.
local function wait(seconds: number)
return Promise.new(function(resolve, _reject, onCancel)
local thread = task.delay(seconds, resolve)
onCancel(function()
task.cancel(thread)
end)
end)
enddefer
Promise.defer(executor: (resolve, reject, onCancel) -> ()): PromiseSame as new, but the executor starts on the next Heartbeat instead of immediately.
resolve / reject
Promise.resolve(...: any): Promise
Promise.reject(...: any): PromiseA Promise that is already resolved or rejected with the given values.
try
Promise.try(callback: (...) -> ...any, ...: any): PromiseCalls callback(...) in a new Promise. Its return values resolve it; an error rejects it.
promisify
Promise.promisify(callback: (...) -> ...any): (...) -> PromiseWraps a yielding function so it returns a Promise instead of yielding.
local getAsync = Promise.promisify(function(key)
return store:GetAsync(key)
end)
getAsync("settings"):andThen(print)delay
Promise.delay(seconds: number): Promise<number>Resolves after seconds with the time actually waited. Values below one frame (1/60 s) are rounded up. Cancelling it stops the timer.
fromEvent
Promise.fromEvent(event, predicate: ((...) -> boolean)?): PromiseResolves the next time event fires (an RBXScriptSignal or a Signal) and the optional predicate returns true. The predicate must return a boolean.
Promise.fromEvent(player.CharacterAdded):andThen(function(character)
print("spawned", character)
end)Combining promises
all
Promise.all(promises: { Promise<T> }): Promise<{ T }>Resolves with every value in order once all resolve. Rejects as soon as one rejects, and cancels the rest.
allSettled
Promise.allSettled(promises: { Promise<T> }): Promise<{ Status }>Waits for every Promise to settle, never rejects, and resolves with each one's final Status.
race
Promise.race(promises: { Promise<T> }): Promise<T>Settles the same way as the first Promise to settle, and cancels the others.
any / some
Promise.any(promises: { Promise<T> }): Promise<T>
Promise.some(promises: { Promise<T> }, count: number): Promise<{ T }>any resolves with the first value to resolve. some resolves with the first count values. Both reject only when too many reject for the goal to be reachable.
each
Promise.each(list: { T | Promise<T> }, predicate: (value: T, index: number) -> TReturn | Promise<TReturn>): Promise<{ TReturn }>Runs predicate on each item one after the other, waiting for any Promise it returns before moving on. Resolves with all results.
fold
Promise.fold(list, reducer: (accumulator, value, index) -> TReturn | Promise<TReturn>, initialValue): Promise<TReturn>Reduces the list into one value, waiting for each step.
retry / retryWithDelay
Promise.retry(callback: (...) -> Promise, times: number, ...: any): Promise
Promise.retryWithDelay(callback: (...) -> Promise, times: number, seconds: number, ...: any): PromiseCalls callback(...) and, if the returned Promise rejects, tries again up to times more times. retryWithDelay waits seconds between attempts.
is
Promise.is(object: any): booleanWhether object is a Promise.
onUnhandledRejection
Promise.onUnhandledRejection(callback: (promise, ...any) -> ()): () -> ()Calls callback for every unhandled rejection, for example to send it to StatisticsService. Returns a function that removes the callback.
Promise methods
andThen
promise:andThen(successHandler: (T...) -> ...any, failureHandler: ((...any) -> ...any)?): PromiseReturns a new Promise that resolves with whatever the handler returns. If the handler returns a Promise, the chain waits for it.
catch
promise:catch(failureHandler: (...any) -> ...any): PromiseHandles a rejection. The returned Promise resolves with the handler's return values, so the chain can recover.
tap
promise:tap(tapHandler: (T...) -> ...any): Promise<T...>Runs a side effect on success and passes the original values through unchanged.
finally
promise:finally(handler: (status: Status) -> ...any): PromiseRuns once the Promise settles in any way, including cancellation. Use it for clean-up.
andThenCall / andThenReturn / finallyCall / finallyReturn
promise:andThenCall(callback, ...args): Promise
promise:andThenReturn(...values): Promise
promise:finallyCall(callback, ...args): Promise
promise:finallyReturn(...values): PromiseShortcuts that call a function with fixed arguments, or return fixed values, without writing a wrapper function.
await / awaitStatus / expect
promise:await(): (boolean, T...)
promise:awaitStatus(): (Status, T...)
promise:expect(): T...Yield until the Promise settles. await returns true plus the values when it resolved, or false plus the rejection. expect returns the values or raises the rejection as an error.
timeout
promise:timeout(seconds: number, rejectionValue: any?): Promise<T...>Rejects with a TimedOut error (or your rejectionValue) if the Promise has not settled within seconds.
remote:RequestAsync("GetInventory")
:timeout(5)
:catch(function(err)
warn("inventory request failed:", tostring(err))
end)now
promise:now(rejectionValue: any?): PromiseResolves right away if the Promise is already resolved, otherwise rejects with a NotResolvedInTime error.
cancel
promise:cancel()Cancels the Promise if it is still started. Its cancel handlers run, Promises chained from it are cancelled too, and a parent Promise is cancelled once none of its consumers still need it.
Status
promise.Status: StatusThe current state, read-only.
Errors
Rejections created by the library itself (timeouts, :now(), errors inside handlers) are Promise.Error objects. tostring(err) gives a readable message with the trace.
| Member | Description |
|---|---|
Promise.Error.Kind | ExecutionError, AlreadyCancelled, NotResolvedInTime, TimedOut. |
Promise.Error.is(value) | Whether a value is a Promise.Error. |
Promise.Error.isKind(value, kind) | Whether a value is a Promise.Error of that kind. |
err.error | The message. |
err.kind | The kind, if any. |
err.trace / err.context | Where it happened. |
err:getErrorChain() | This error and every parent error, in order. |
promise:catch(function(err)
if Promise.Error.isKind(err, Promise.Error.Kind.TimedOut) then
warn("took too long")
end
end)