Skip to content

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.

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

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

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

swoftlang
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
    }
}
txt
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.

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

FormArgumentResult
all of LL : List<Future<T>>Future<List<T>> — all results, input order
any of LL : 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.

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

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

swoftlang
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

ConstructRequiresYieldsContext
spawn callcall is an async callable returning RFuture<R>any color
async { … x }x : T (trailing expr)Future<T>any color
await ee : Future<T>Tasync only (same gate as wait)
all of LL : List<Future<T>>Future<List<T>>any color
any of LL : 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 detached async { } task that awaited it stops at that await; the error is logged.
  • Cancellation on reload or shutdown (the runtime cancels every in-flight task): a pending await unwinds 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 the Optional; 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 the await, 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