The Skull Brig: failure contexts and the <decides> operator

In this lesson you'll learn to

  • βœ“Student can write failable functions with <decides><transacts> and call them inside if-contexts so failures roll back instead of crashing.

Student can write failable functions with <decides><transacts> and call them inside if-contexts so failures roll back instead of crashing.

πŸ“– Reference & full walkthrough: The Skull Brig: failure contexts and the <decides> operator

🧩 Your capstone piece: Failable checkpoint validation: ValidateCheckpoint[Racer, Index]<decides> gates lap progress (FAILS compile 2 err β€” rewrite; also compile-gate verse-failure-contexts which has no compile key; passing failure-context-required + failure-decides back it)


What you'll learn

You'll learn how Verse's failure contexts replace boolean flags. Instead of returning true/false and branching on it, you write code that succeeds when a condition holds and fails (yields zero values) when it doesn't. When a <decides> expression fails inside an if: condition, execution jumps straight to else: β€” with automatic rollback of any speculative state. We'll pair that with a real UI widget: on failure we call GetPlayerUI[...] and AddWidget to display a text_block error message the player can actually see.

How it works

  1. The <decides> effect: A function marked <decides> can fail. IsEnabled[] (note the [] β€” fallible call) succeeds when the device is enabled and fails when it's disabled. It comes from the enableable interface β€” only some devices implement it (like hiding_prop_device); a plain trigger_device does not have IsEnabled. You may ONLY call <decides> functions inside a failure context (an if/for condition, another <decides> body, or a not/or operand). Never call one as a bare statement.
  2. The failure context: Inside if (A[]): every expression must succeed for the then: block to run. The moment one fails, Verse skips to else: and rolls back anything speculative.
  3. Real UI feedback: GetPlayerUI[Player] is also <decides> β€” bind it in the condition (if (UI := GetPlayerUI[Player]):), never call it bare. Then build a text_block β€” its DefaultText is a message, not a string, so pass your text through a <localizes> function β€” and hand it to player_ui.AddWidget. That's the payload the player sees.
  4. Cleanup: We keep the widget in a ?text_block so we can RemoveWidget it before showing the next message, avoiding stacked errors.

Let's build it

This device listens on a trigger. Each activation it validates a target hiding prop (think: a barrel below decks in the Skull Brig) via a <decides> helper. On success it prints a confirmation; on failure it renders an error text_block on that player's UI.

using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/Diagnostics }

# Localisation helper β€” UI text must be message-typed, so route strings through <localizes>.
BrigErrorText<localizes>(Text : string) : message = "Error: {Text}"

# Demonstrates failure contexts driving real player-UI feedback.
failure_feedback_device := class<concrete>(creative_device):

    # Trigger the player activates to attempt the interaction.
    @editable
    ActivationTrigger : trigger_device = trigger_device{}

    # The hiding prop (a barrel fits the Skull Brig) we validate before acting on it.
    # hiding_prop_device implements the enableable interface, so it exposes IsEnabled[].
    @editable
    TargetDevice : hiding_prop_device = hiding_prop_device{}

    # Track the last error widget so we can remove it before showing a new one.
    var MaybeErrorWidget : ?text_block = false

    # <decides>: succeeds only if the target is enabled; fails as control flow otherwise.
    ValidateTarget(Device : hiding_prop_device)<transacts><decides> : void =
        Device.IsEnabled[]   # fallible call β€” this line IS the success condition

    # Push an error message onto the given player's UI.
    ShowError(Player : player, Text : string) : void =
        # GetPlayerUI is <decides>: bind it in the condition, never call bare.
        if (PlayerUI := GetPlayerUI[Player]):
            # Remove a previous error so messages don't stack up.
            if (Old := MaybeErrorWidget?):
                PlayerUI.RemoveWidget(Old)
            # Build the widget β€” DefaultText is a message, so use the <localizes> helper.
            ErrorText := text_block{DefaultText := BrigErrorText(Text)}
            PlayerUI.AddWidget(ErrorText)
            set MaybeErrorWidget = option{ErrorText}

    # Runs when the trigger fires. Payload is the activating agent (may be empty).
    OnActivated(MaybeAgent : ?agent) : void =
        # Bind the agent and resolve it to a player, all in one failure context.
        if (Agent := MaybeAgent?, Player := player[Agent]):
            if (ValidateTarget[TargetDevice]):   # succeeds only if TargetDevice is enabled
                Print("Interaction succeeded!")
            else:
                # Runs automatically when ValidateTarget fails.
                ShowError(Player, "Target device is disabled")

    OnBegin<override>()<suspends> : void =
        # Device events subscribe WITHOUT parentheses on the event field.
        ActivationTrigger.TriggeredEvent.Subscribe(OnActivated)

Try it yourself

  1. Place a Trigger device, a Hiding Prop device (pick the barrel β€” very Skull Brig), plus this device. Assign the trigger to ActivationTrigger and the prop to TargetDevice.
  2. In the TargetDevice details, set Enabled at Game Start to Off so IsEnabled[] fails.
  3. Play the level, walk onto ActivationTrigger, and watch the red text_block error appear on your HUD.
  4. Flip TargetDevice back to Enabled and trigger again β€” now you see the Print confirmation instead of the widget.
  5. Trigger repeatedly while disabled: the previous widget is removed first, so errors never stack.

Recap

  • <decides> functions represent computations that may fail β€” call them with [] and only inside a failure context.
  • An if (Cond[]): runs then: only when every condition succeeds; otherwise it jumps to else: and rolls back speculative state.
  • GetPlayerUI[Player] is itself <decides> β€” bind it, never call it bare.
  • Real feedback means actually building a text_block (its DefaultText is a message β€” use a <localizes> helper, never a raw string), calling AddWidget, and tracking it in a ?text_block so you can RemoveWidget it later.
  • Device events like TriggeredEvent subscribe without parentheses on the event field.
πŸ”’

Keep going β€” free

You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.

Sign in free to continue this lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/sug-implementing-error-handling-with-failure-contexts-and-the-de
Source

Biloxi Studios original lesson

Guided course

Add this lesson to your free study plan.

🧭 The Keeper’s log

Quest complete? Chart your next heading from the πŸ¦‘ West Coves expedition.

β›΅ Take me there