Skip to content
Engine

Async and Events

Promise

Values that arrive later. Every Engine method that ends in Async returns a Promise.

PackageShared

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:

StatusMeaning
"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.
Chaining
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 rejected

Creating promises

new

Promise.new(executor: (resolve, reject, onCancel) -> ()): Promise

Runs 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)
end

defer

Promise.defer(executor: (resolve, reject, onCancel) -> ()): Promise

Same as new, but the executor starts on the next Heartbeat instead of immediately.

resolve / reject

Promise.resolve(...: any): Promise
Promise.reject(...: any): Promise

A Promise that is already resolved or rejected with the given values.

try

Promise.try(callback: (...) -> ...any, ...: any): Promise

Calls callback(...) in a new Promise. Its return values resolve it; an error rejects it.

promisify

Promise.promisify(callback: (...) -> ...any): (...) -> Promise

Wraps 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)?): Promise

Resolves 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): Promise

Calls callback(...) and, if the returned Promise rejects, tries again up to times more times. retryWithDelay waits seconds between attempts.

is

Promise.is(object: any): boolean

Whether 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)?): Promise

Returns 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): Promise

Handles 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): Promise

Runs 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): Promise

Shortcuts 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?): Promise

Resolves 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: Status

The 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.

MemberDescription
Promise.Error.KindExecutionError, 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.errorThe message.
err.kindThe kind, if any.
err.trace / err.contextWhere 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)