Futures
A Future<T> is a value that stands for a result that isn't ready yet — the work is running on a background virtual thread and the future is your handle to its eventual T. It is a real, first-class value: bind it, pass it to a function, return it, store it in a List<Future<T>>. This page is the reference for producing, consuming, and combining futures; the Async guide is the narrative introduction.
Future<T> is generic and PascalCase like List<T> or Optional<T>. You never construct one directly — spawn and async { } produce them.
Producing a future
spawn <call> as an expression
spawn f(args) starts f on its own async task and evaluates to a Future<R>, where R is the inferred return type of the async callable f. Only async callables are spawnable — spawning a plain sync function is an error, the same as it has always been.
async function build_profile(p: Player) {
wait 100 millis
return "gold"
}
async function demo(player: Player) {
set handle to spawn build_profile(player) // Future<String>
set rank to await handle
send "rank ${rank}" to player
}As a bare statement, spawn f(args) is fire-and-forget — the future is simply discarded. Every existing script that used spawn for a detached task is unchanged; becoming an expression is purely additive.
async function log_metrics(p: Player) {
wait 10 millis
return 1
}
command "ping" {
execute {
if sender is a Player {
spawn log_metrics(sender) // statement form → result discarded
send "pong" to sender
}
}
}async { … } as an expression
An async { } block used as an expression evaluates to a Future<T> where T is the type of the block's trailing expression. With no trailing expression the type is Future<Unit>.
async function demo(p: Player) {
set answer to async {
wait 200 millis
6 * 7 // trailing expression → Future<Integer>
}
send "answer ${await answer}" to p
}Consuming a future
A future has a single consumer, await. It is legal only in async color; on the tick thread you detach into an async { } task (or spawn an async function) and await there.
await <future> — async context only
await e requires e : Future<T> and evaluates to T. It parks the current virtual thread until the future resolves (cheap — the same parking wait does) and returns immediately if the result is already present.
await blocks, so it obeys the same color rule as wait: legal only in an async context (an async function body, execute async, or an async { } block), and a compile error on the tick thread, where blocking would freeze the world.
async function score(p: Player) {
wait 50 millis
return 10
}
Player {
on_join {
set s to await spawn score(player)
send "score ${s}" to player
}
}await_tick.sw:8:18: error: 'await' is only allowed in async functions, 'execute async', or 'async { }' blocks; on the tick thread wrap the awaiting work in an 'async { }' block instead
set s to await spawn score(player)
^Detaching on the tick thread
Tick-colored code cannot await directly, so it detaches the follow-up into an async { } task (or spawns a whole async function). Inside that task await is legal; the world access afterward auto-hops back onto the tick thread, so it is safe to touch the world right after the await.
struct Profile { rank: String }
async function build_profile(p: Player) {
wait 100 millis
return Profile { rank: "gold" }
}
Player {
on_join {
async {
set profile to await spawn build_profile(player)
send "welcome ${profile.rank}" to player
}
}
}If the future completes exceptionally or is cancelled, the detached task stops at the await and the code after it does not run (see Error model).
Combinators
all of and any of turn a list of futures into a single future, so several tasks wait in parallel instead of one after another.
| Form | Argument | Result |
|---|---|---|
all of L | L : List<Future<T>> | Future<List<T>> — all results, input order |
any of L | L : List<Future<T>> | Future<T> — the first result to arrive |
all of resolves once the slowest input does; any of resolves as soon as the first input does (a race). Both are ordinary future-valued expressions — await them in async code, or from tick code await them inside a detached async { } task.
async function ping(mirror: String) {
wait 100 millis
return mirror
}
async function pick(p: Player) {
set mirrors to [spawn ping("eu"), spawn ping("us"), spawn ping("asia")]
set all_replies to await all of mirrors // List<String>, all three
set first_reply to await any of mirrors // String, whichever won
send "${all_replies.size} replied, ${first_reply} first" to p
}Positional destructure
all of over a list literal of futures of different types can be destructured positionally: the compiler tracks each element's type by position, so set (a, b) to await all of [fa, fb] binds a : Ta and b : Tb. This is the way to fan out a fixed set of heterogeneous work; the homogeneous Future<List<T>> form above is for same-typed sets.
struct Stats { kills: Integer }
struct Friends { count: Integer }
async function load_stats(p: Player) {
wait 300 millis
return Stats { kills: 12 }
}
async function load_friends(p: Player) {
wait 500 millis
return Friends { count: 7 }
}
async function open_menu(p: Player) {
set (stats, friends) to await all of [spawn load_stats(p), spawn load_friends(p)]
send "${stats.kills} kills · ${friends.count} friends" to p
}Holding a future
A future is a value, so it need not be consumed where it is produced. Start slow work early, carry the handle through unrelated code, and await it only when the value is needed — the wait overlaps with everything in between. A future can also be a parameter, letting one function start the work and another finish it.
async function slow_lookup(id: Integer) {
wait 500 millis
return id * 10
}
async function report(p: Player, pending: Future<Integer>) {
send "working..." to p
set value to await pending // finish work started elsewhere
send "result ${value}" to p
}
async function process(p: Player, id: Integer) {
set handle to spawn slow_lookup(id) // starts now
report(p, handle) // hand the handle off
}Typing & color rules
| Construct | Requires | Yields | Context |
|---|---|---|---|
spawn call | call is an async callable returning R | Future<R> | any color |
async { … x } | x : T (trailing expr) | Future<T> | any color |
await e | e : Future<T> | T | async only (same gate as wait) |
all of L | L : List<Future<T>> | Future<List<T>> | any color |
any of L | L : List<Future<T>> | Future<T> | any color |
Spawning a non-async callable is an error. Everything else about async — world access hopping back to the tick thread, scope snapshots, halt killing only its own task — is unchanged; futures add a value handle, not a new execution model.
Error model
There is deliberately no error branch on a future in this release — no otherwise, no with timeout, no Result<T> / if it failed. The reason is that nothing a future wraps today does outbound IO: SwoftLang has inbound api routes but no outbound HTTP, webhook, or database call — so there is no operation whose failure is a normal, expected outcome worth branching on. The failure modes that do exist are handled without one:
- A runtime error inside the async body completes the future exceptionally.
awaitre-raises it in the awaiting task, where it propagates and is logged like any script error — exactly as a sequential call would. A detachedasync { }task that awaited it stops at thatawait; the error is logged. - Cancellation on reload or shutdown (the runtime cancels every in-flight task): a pending
awaitunwinds the task cleanly and every detached task stops. The program is being torn down — not a handled outcome. - Missing data is not a future failure — model it as
Optional<T>. The future succeeds with theOptional; don't route absence through the future. - A player or entity gone mid-load is not a future failure — the value succeeded. Re-check liveness (
p.online) after theawait, the usual stale-player guard.
An await f otherwise <default>, await f with timeout <dur>, and a Result<T> / if it failed as err branch are deferred until outbound IO lands — that is the first operation whose failure is a routine, expected result, and its error model will be designed alongside it.
See also
- Async guide — the narrative walkthrough with a combined example
- Structs and Persistence — where the "not-ready-yet" values (loaded records) typically come from
- Options — how missing data is modeled, not as a future failure