Why Verse Fails #2 — The Effects System
Lesson beginner

Why Verse Fails #2 — The Effects System

beginner Verse

Overview

Last lesson you learned that some Verse expressions are questions — they carry the decides effect and must live where failure has a plan. Here's the bigger picture that turns those errors from annoyances into a system you can reason about: every Verse function has a contract, written in angle brackets, and the compiler holds everyone to it.

<decides> — this function might fail. <suspends> — this function takes time. <transacts> — everything this function does can be rolled back. And the one nobody writes but everyone gets: a function with no annotation defaults to no_rollback — "what I do cannot be undone." That silent default is behind more compile failures than any other single fact in Verse. Today the vault teaches it three ways.

You'll meet error 3512 again in this lesson — but where lesson 1's 3512 said the 'decides' effect wasn't allowed, today's says 'no_rollback'. Same error number, same sentence shape, different effect named. 3512 isn't "the decides error" — it's the effects-contract error, and once you can read which effect it names, you can fix every variant.

Why rollback matters at all

A failure context doesn't just detect failure — it undoes whatever the attempt did, as if it never happened. if (TrySpend[Player, 50]): that fails halfway must leave the player's balance untouched. So anything executed inside a failure context must be undoable — rollback-safe — which is exactly what <transacts> promises. And that's why no_rollback code is banned from conditions: the compiler cannot un-ring that bell.

Wall 1 — the silent default

A tiny helper that scores keys, used in a condition:

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

vault_score_d := class(creative_device):
    KeyValue(KeyCount : int) : int = KeyCount * 10

    OnBegin<override>()<suspends> : void =
        if (KeyValue(3) > 20):
            Print("The vault hums with approval")
Script error 3512: This invocation calls a function that has the 'no_rollback' effect, which is not allowed by its context.

KeyValue multiplies two ints. It couldn't be safer to undo — but you never said so, and an unannotated function defaults to no_rollback. The condition of an if is a failure context; failure contexts demand rollback-safe work; the default breaks the contract. The fix is one annotation, a promise you can honestly make:

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

vault_score_d := class(creative_device):
    KeyValue(KeyCount : int)<transacts> : int = KeyCount * 10

    OnBegin<override>()<suspends> : void =
        if (KeyValue(3) > 20):
            Print("The vault hums with approval")

Rule of thumb: annotate pure helpers <transacts>. If a function only computes — no device calls, no world changes — give it the annotation, and it becomes welcome in any condition.

Wall 2 — a promise you can't make

The opposite case. This helper grants a real item to a real player, and we've optimistically marked it <transacts>:

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

vault_grant_e := class(creative_device):
    @editable
    KeycardGranter : item_granter_device = item_granter_device{}

    GiveKey(Agent : agent)<transacts> : void =
        KeycardGranter.GrantItemIndex(Agent, 0)

    OnBegin<override>()<suspends> : void =
        Print("Vault attendant ready")
Script error 3512: This invocation calls a function that has the 'no_rollback' effect, which is not allowed by its context.

Now read the error from the other direction: the caller promised rollback (<transacts>), but GrantItemIndex is a native engine call that changes the actual world — it carries no_rollback, and no annotation on your wrapper can change what it does. You can't promise to undo a granted item. The honest fix is to stop promising:

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

vault_grant_e := class(creative_device):
    @editable
    KeycardGranter : item_granter_device = item_granter_device{}

    GiveKey(Agent : agent) : void =
        KeycardGranter.GrantItemIndex(Agent, 0)

    OnBegin<override>()<suspends> : void =
        Print("Vault attendant ready")

Leave functions that touch the world undecorated. That's not a defeat — it's the contract system working: your function's signature now tells every caller "this really happens."

Wall 3 — effects are transitive (and the compiler settles a myth)

The subtle one. A label helper that builds a string, called from a <transacts> function:

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

vault_label_f := class(creative_device):
    CoinLabel(N : int) : string = "{N} coins"

    Price(Base : int)<transacts> : string =
        CoinLabel(Base * 2)

    OnBegin<override>()<suspends> : void =
        Print("Price board ready")
Script error 3512: This invocation calls a function that has the 'no_rollback' effect, which is not allowed by its context.

Nothing here touches the world — it's string building! But CoinLabel is unannotated, so it defaults to no_rollback, and Price promised <transacts> — a promise-keeper may only call other promise-keepers. Effects flow through call chains. One unannotated link poisons every transactional caller above it.

And here's the part worth pinning, because it's widely mis-remembered: string interpolation itself is perfectly legal inside <transacts>. Watch:

Same interpolation, one annotation added, PASS. It was never the "{N} coins" that broke the build — it was the silent default on the helper that contains it. When a build fails "because of string interpolation," look at the annotation on the function doing the interpolating.

The mental model to keep

  • Angle brackets are contracts. <decides> = might fail · <suspends> = takes time · <transacts> = fully undoable · unannotated = no_rollback, cannot be undone.
  • The silent default is the trap. Pure compute helpers should say <transacts>; you have to opt IN to being rollback-safe.
  • Failure contexts undo — so they only admit undoable work. That's the deep reason conditions reject no_rollback.
  • Some things truly can't roll back. Native world-changing calls (granting items, attaching components) are honestly no_rollback — leave their callers undecorated rather than promising the impossible.
  • Effects are transitive. A <transacts> function may only call <transacts> (or stricter) functions. One unannotated helper breaks the chain — annotate at the source.
  • 3512 names the effect it caught. Read past the number to the quoted effect: 'decides' means an unplanned question; 'no_rollback' means an un-undoable action in a place that must undo.

Where this goes next

You now hold the two core ideas: failure has contexts (lesson 1), and functions have contracts (this lesson). Next: the no_rollback family — the four specific shapes that sneak un-undoable work into condition positions (allocation, native calls, string-building, and a for loop's iterable), each with its real error and its hoist-or-annotate fix.

Get the complete code — free

You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.

Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.

Check your understanding

Test yourself with an interactive quiz and track your progress + earn XP — free for members.

Turn this into a guided course

Add Function contracts: <transacts>, the silent no_rollback default, and effect transitivity — reading 3512's second face to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.

Original lesson generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.

Comments

    Sign in to vote, comment, or suggest an edit. Sign in