Reference Verse compiles

race: Run Two Paths, Keep the Winner

Sometimes you want two things to happen at once — a countdown timer AND a player reaching a finish line — and you only care which one completes first. Verse's `race` expression is the answer: it launches two (or more) suspending branches simultaneously and cancels all the others the moment one finishes. No manual cancellation tokens, no flag variables — the language handles it for you.

Updated Examples verified on the live UEFN compiler
Watch the Knotrace in ~90 seconds.

Overview

The race expression is a Verse language built-in (not a UEFN device), so it lives in every Verse file without any extra using import. It belongs to Verse's structured concurrency family alongside sync, rush, and branch.

race:
    BranchA()   # suspending call
    BranchB()   # suspending call

race starts all branches at the same time. The instant one branch returns, race itself returns and every other branch is automatically cancelled — their defer blocks run, any held locks are released, and execution continues after the race block.

When to reach for it:

  • A round timer vs. a player completing an objective — whichever fires first ends the round.
  • A "first player to reach the dock wins" race against a storm closing in.
  • Waiting for a player to interact with a device OR for them to be eliminated — handle whichever happens first.
  • Any "timeout" pattern: do the real work, but give up after N seconds if it hasn't finished.

race is the right tool whenever you have two or more competing async outcomes and only the winner matters.

API Reference

(API surface could not be resolved for this device.)

Walkthrough

Scenario — Clifftop Cove Sprint: Players spawn at a sunny clifftop dock. A 30-second countdown begins. The first player to step on the finish-line trigger plate at the cove shore wins. If the timer expires before anyone reaches the shore, the round ends in a timeout. We use race so that whichever happens first — a player triggering the plate OR the 30-second clock — immediately ends the round.

Place these devices in your UEFN level:

  • FinishTrigger — a trigger_device at the cove shore
  • RoundEndNotifier — a second trigger_device used to signal round-over to other devices (e.g. a cinematic sequence)
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

# Localized message helper
RoundMessage<localizes>(S : string) : message = "{S}"

cove_race_manager := class(creative_device):

    # The trigger plate at the cove shore — player steps on it to finish
    @editable
    FinishTrigger : trigger_device = trigger_device{}

    # A second trigger we activate to fire downstream devices (e.g. end-round cinematic)
    @editable
    RoundEndNotifier : trigger_device = trigger_device{}

    # How long (seconds) before the round times out
    @editable
    RoundDuration : float = 30.0

    # Tracks whether a player won (vs. timeout)
    var PlayerWon : logic = false

    # Called by the engine when the island starts
    OnBegin<override>()<suspends> : void =
        # Subscribe to the finish trigger so we can record the winner
        FinishTrigger.TriggeredEvent.Subscribe(OnPlayerFinished)

        # race: timer vs. player reaching the shore
        # Whichever branch returns first wins; the other is cancelled
        race:
            WaitForPlayerFinish()   # suspends until FinishTrigger fires
            RunCountdown()          # suspends for RoundDuration seconds

        # We reach here only after one branch completes
        if (PlayerWon?):
            Print("A player reached the cove — round over, player wins!")
        else:
            Print("Time's up! The storm swallowed the dock.")

        # Signal downstream devices regardless of who won
        RoundEndNotifier.Trigger()

    # Branch 1 — suspends until the finish trigger fires
    WaitForPlayerFinish()<suspends> : void =
        # Await blocks the current branch until the event fires once
        FinishTrigger.TriggeredEvent.Await()
        set PlayerWon = true

    # Branch 2 — suspends for the full round duration
    RunCountdown()<suspends> : void =
        Sleep(RoundDuration)
        # If we reach here, the timer expired before any player finished

    # Event handler — called whenever the trigger fires (for any subscriber)
    OnPlayerFinished(Agent : ?agent) : void =
        # Unwrap the optional agent safely
        if (A := Agent?):
            Print("Player crossed the finish line at the cove shore!")

Line-by-line breakdown:

Lines What's happening
@editable fields Wire up FinishTrigger and RoundEndNotifier in the UEFN Details panel — no bare identifiers.
FinishTrigger.TriggeredEvent.Subscribe(OnPlayerFinished) Registers OnPlayerFinished to run every time the plate fires, for logging/side effects.
race: block Launches WaitForPlayerFinish and RunCountdown simultaneously.
WaitForPlayerFinish Calls .Await() on the trigger's event — this suspends the branch until the event fires exactly once.
RunCountdown Calls Sleep(RoundDuration) — suspends for 30 seconds.
After race: Exactly one branch returned; the other was cancelled. We check PlayerWon to decide the message.
RoundEndNotifier.Trigger() Fires a second device to kick off end-round cinematics or scoring.

Common patterns

Pattern 1 — Timeout wrapper (race a task against a deadline)

A common use of race is giving any async operation a hard deadline. Here a player must interact with a glowing buoy device within 10 seconds or the opportunity is lost.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

buoy_interaction_manager := class(creative_device):

    @editable
    BuoyButton : button_device = button_device{}

    @editable
    MissedOpportunityNotifier : trigger_device = trigger_device{}

    var InteractionSucceeded : logic = false

    OnBegin<override>()<suspends> : void =
        race:
            WaitForBuoyPress()
            TimeoutBranch()

        if (InteractionSucceeded?):
            Print("Buoy activated — treasure chest unlocked!")
        else:
            Print("Too slow — the buoy sank back into the cove.")
            MissedOpportunityNotifier.Trigger()

    WaitForBuoyPress()<suspends> : void =
        BuoyButton.InteractedWithEvent.Await()
        set InteractionSucceeded = true

    TimeoutBranch()<suspends> : void =
        Sleep(10.0)

Pattern 2 — Race three competing events

race accepts more than two branches. Here three pressure plates are scattered across a sunny dock area; the first player to step on any plate wins the round.

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

three_plate_race := class(creative_device):

    @editable
    PlateA : trigger_device = trigger_device{}

    @editable
    PlateB : trigger_device = trigger_device{}

    @editable
    PlateC : trigger_device = trigger_device{}

    @editable
    WinnerAnnouncer : trigger_device = trigger_device{}

    # Which plate index won (1, 2, or 3)
    var WinningPlate : int = 0

    OnBegin<override>()<suspends> : void =
        race:
            AwaitPlate(PlateA, 1)
            AwaitPlate(PlateB, 2)
            AwaitPlate(PlateC, 3)

        Print("Plate {WinningPlate} was reached first!")
        WinnerAnnouncer.Trigger()

    AwaitPlate(Plate : trigger_device, Index : int)<suspends> : void =
        Plate.TriggeredEvent.Await()
        set WinningPlate = Index

Pattern 3 — Race with player elimination check

Race a player completing a shore objective against them being eliminated. If they're eliminated first, cancel the objective gracefully.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /Verse.org/Native }

shore_objective_manager := class(creative_device):

    @editable
    ShoreFinishTrigger : trigger_device = trigger_device{}

    @editable
    ObjectiveCompleteNotifier : trigger_device = trigger_device{}

    @editable
    PlayerSpawner : player_spawner_device = player_spawner_device{}

    var ObjectiveComplete : logic = false

    OnBegin<override>()<suspends> : void =
        # Wait for a player to spawn before starting the objective race
        SpawnedAgent := PlayerSpawner.SpawnedEvent.Await()

        if (Agent := SpawnedAgent?,
            FortChar := Agent.GetFortCharacter[]):
            race:
                WaitForShoreFinish()
                WaitForElimination(FortChar)

            if (ObjectiveComplete?):
                Print("Player reached the shore — objective complete!")
                ObjectiveCompleteNotifier.Trigger()
            else:
                Print("Player was eliminated before reaching the shore.")

    WaitForShoreFinish()<suspends> : void =
        ShoreFinishTrigger.TriggeredEvent.Await()
        set ObjectiveComplete = true

    WaitForElimination(Char : fort_character)<suspends> : void =
        Char.EliminatedEvent().Await()

Gotchas

1. Both branches must be <suspends> calls. If a branch returns immediately (no Sleep, no .Await()), race completes instantly on that branch and the other branch never gets a chance to run. Always make sure every branch actually suspends.

2. Cancelled branches run their defer blocks. If a losing branch has defer statements (for cleanup), those will execute when the branch is cancelled. This is usually what you want, but be aware: don't put side-effect logic you only want on success inside a defer in a race branch.

3. race is not the same as sync.

  • sync waits for all branches to finish.
  • race waits for one branch and cancels the rest. Using sync when you meant race means your round never ends until both the timer AND the player finish — a classic bug.

4. Shared mutable state needs care. Both branches can write to the same var field. In Pattern 1 and the Walkthrough, the winning branch sets a flag (PlayerWon, InteractionSucceeded) and the losing branch never gets to set it because it's cancelled. This is safe, but if both branches could theoretically complete at the exact same simulation tick, the last writer wins — design your flags accordingly.

5. race only works inside a <suspends> context. You cannot call race from a plain (non-suspending) function or from a Subscribe handler directly. Put your race block inside OnBegin<override>()<suspends> or another <suspends> function.

6. Localized text for messages. If you pass text to a device method that expects a message type, you cannot pass a raw string. Declare a localizer helper:

MyLabel<localizes>(S : string) : message = "{S}"

There is no StringToMessage function in Verse.

Guides & scripts that use race

Step-by-step tutorials that put this object to work.

Build your own lesson with race

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →